mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Customize for Roo
This commit is contained in:
5
.changeset/large-humans-exist.md
Normal file
5
.changeset/large-humans-exist.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"roo-cline": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add auto-approve menu (thanks Cline!)
|
||||||
@@ -93,6 +93,7 @@ export interface ExtensionState {
|
|||||||
mode: Mode
|
mode: Mode
|
||||||
modeApiConfigs?: Record<Mode, string>
|
modeApiConfigs?: Record<Mode, string>
|
||||||
enhancementApiConfigId?: string
|
enhancementApiConfigId?: string
|
||||||
|
autoApprovalEnabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClineMessage {
|
export interface ClineMessage {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||||
import { useCallback, useState } from "react"
|
import { useCallback, useState } from "react"
|
||||||
import styled from "styled-components"
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
|
|
||||||
interface AutoApproveAction {
|
interface AutoApproveAction {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
shortName: string
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,58 +14,97 @@ interface AutoApproveMenuProps {
|
|||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_MAX_REQUESTS = 50
|
|
||||||
|
|
||||||
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false)
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
const [actions, setActions] = useState<AutoApproveAction[]>([
|
const {
|
||||||
|
alwaysAllowReadOnly,
|
||||||
|
setAlwaysAllowReadOnly,
|
||||||
|
alwaysAllowWrite,
|
||||||
|
setAlwaysAllowWrite,
|
||||||
|
alwaysAllowExecute,
|
||||||
|
setAlwaysAllowExecute,
|
||||||
|
alwaysAllowBrowser,
|
||||||
|
setAlwaysAllowBrowser,
|
||||||
|
alwaysAllowMcp,
|
||||||
|
setAlwaysAllowMcp,
|
||||||
|
alwaysApproveResubmit,
|
||||||
|
setAlwaysApproveResubmit,
|
||||||
|
autoApprovalEnabled,
|
||||||
|
setAutoApprovalEnabled,
|
||||||
|
} = useExtensionState()
|
||||||
|
|
||||||
|
const actions: AutoApproveAction[] = [
|
||||||
{
|
{
|
||||||
id: "readFiles",
|
id: "readFiles",
|
||||||
label: "Read files and directories",
|
label: "Read files and directories",
|
||||||
enabled: false,
|
shortName: "Read",
|
||||||
|
enabled: alwaysAllowReadOnly ?? false,
|
||||||
description: "Allows access to read any file on your computer.",
|
description: "Allows access to read any file on your computer.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "editFiles",
|
id: "editFiles",
|
||||||
label: "Edit files",
|
label: "Edit files",
|
||||||
enabled: false,
|
shortName: "Edit",
|
||||||
|
enabled: alwaysAllowWrite ?? false,
|
||||||
description: "Allows modification of any files on your computer.",
|
description: "Allows modification of any files on your computer.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "executeCommands",
|
id: "executeCommands",
|
||||||
label: "Execute safe commands",
|
label: "Execute safe commands",
|
||||||
enabled: false,
|
shortName: "Commands",
|
||||||
|
enabled: alwaysAllowExecute ?? false,
|
||||||
description:
|
description:
|
||||||
"Allows automatic execution of safe terminal commands. The model will determine if a command is potentially destructive and ask for explicit approval.",
|
"Allows execution of approved terminal commands. You can configure this in the settings panel.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "useBrowser",
|
id: "useBrowser",
|
||||||
label: "Use the browser",
|
label: "Use the browser",
|
||||||
enabled: false,
|
shortName: "Browser",
|
||||||
|
enabled: alwaysAllowBrowser ?? false,
|
||||||
description: "Allows ability to launch and interact with any website in a headless browser.",
|
description: "Allows ability to launch and interact with any website in a headless browser.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "useMcp",
|
id: "useMcp",
|
||||||
label: "Use MCP servers",
|
label: "Use MCP servers",
|
||||||
enabled: false,
|
shortName: "MCP",
|
||||||
|
enabled: alwaysAllowMcp ?? false,
|
||||||
description: "Allows use of configured MCP servers which may modify filesystem or interact with APIs.",
|
description: "Allows use of configured MCP servers which may modify filesystem or interact with APIs.",
|
||||||
},
|
},
|
||||||
])
|
{
|
||||||
const [maxRequests, setMaxRequests] = useState(DEFAULT_MAX_REQUESTS)
|
id: "retryRequests",
|
||||||
const [enableNotifications, setEnableNotifications] = useState(false)
|
label: "Retry failed requests",
|
||||||
|
shortName: "Retries",
|
||||||
|
enabled: alwaysApproveResubmit ?? false,
|
||||||
|
description: "Automatically retry failed API requests when the provider returns an error response.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const toggleExpanded = useCallback(() => {
|
const toggleExpanded = useCallback(() => {
|
||||||
setIsExpanded((prev) => !prev)
|
setIsExpanded((prev) => !prev)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toggleAction = useCallback((actionId: string) => {
|
const enabledActionsList = actions
|
||||||
setActions((prev) =>
|
.filter((action) => action.enabled)
|
||||||
prev.map((action) => (action.id === actionId ? { ...action, enabled: !action.enabled } : action)),
|
.map((action) => action.shortName)
|
||||||
)
|
.join(", ")
|
||||||
}, [])
|
|
||||||
|
|
||||||
const enabledActions = actions.filter((action) => action.enabled)
|
// Individual checkbox handlers - each one only updates its own state
|
||||||
const enabledActionsList = enabledActions.map((action) => action.label).join(", ")
|
const handleReadOnlyChange = useCallback(() => setAlwaysAllowReadOnly(!(alwaysAllowReadOnly ?? false)), [alwaysAllowReadOnly, setAlwaysAllowReadOnly])
|
||||||
|
const handleWriteChange = useCallback(() => setAlwaysAllowWrite(!(alwaysAllowWrite ?? false)), [alwaysAllowWrite, setAlwaysAllowWrite])
|
||||||
|
const handleExecuteChange = useCallback(() => setAlwaysAllowExecute(!(alwaysAllowExecute ?? false)), [alwaysAllowExecute, setAlwaysAllowExecute])
|
||||||
|
const handleBrowserChange = useCallback(() => setAlwaysAllowBrowser(!(alwaysAllowBrowser ?? false)), [alwaysAllowBrowser, setAlwaysAllowBrowser])
|
||||||
|
const handleMcpChange = useCallback(() => setAlwaysAllowMcp(!(alwaysAllowMcp ?? false)), [alwaysAllowMcp, setAlwaysAllowMcp])
|
||||||
|
const handleRetryChange = useCallback(() => setAlwaysApproveResubmit(!(alwaysApproveResubmit ?? false)), [alwaysApproveResubmit, setAlwaysApproveResubmit])
|
||||||
|
|
||||||
|
// Map action IDs to their specific handlers
|
||||||
|
const actionHandlers: Record<AutoApproveAction['id'], () => void> = {
|
||||||
|
readFiles: handleReadOnlyChange,
|
||||||
|
editFiles: handleWriteChange,
|
||||||
|
executeCommands: handleExecuteChange,
|
||||||
|
useBrowser: handleBrowserChange,
|
||||||
|
useMcp: handleMcpChange,
|
||||||
|
retryRequests: handleRetryChange,
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -86,39 +126,41 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={toggleExpanded}>
|
onClick={toggleExpanded}>
|
||||||
<VSCodeCheckbox
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
checked={enabledActions.length > 0}
|
<VSCodeCheckbox
|
||||||
onChange={(e) => {
|
checked={autoApprovalEnabled ?? false}
|
||||||
const checked = (e.target as HTMLInputElement).checked
|
onChange={() => setAutoApprovalEnabled(!(autoApprovalEnabled ?? false))}
|
||||||
setActions((prev) =>
|
/>
|
||||||
prev.map((action) => ({
|
</div>
|
||||||
...action,
|
<div style={{
|
||||||
enabled: checked,
|
display: 'flex',
|
||||||
})),
|
alignItems: 'center',
|
||||||
)
|
gap: '4px',
|
||||||
e.stopPropagation()
|
flex: 1,
|
||||||
}}
|
minWidth: 0
|
||||||
onClick={(e) => e.stopPropagation()}
|
}}>
|
||||||
/>
|
<span style={{
|
||||||
<CollapsibleSection>
|
color: "var(--vscode-foreground)",
|
||||||
<span style={{ color: "var(--vscode-foreground)" }}>Auto-approve:</span>
|
flexShrink: 0
|
||||||
<span
|
}}>Auto-approve:</span>
|
||||||
style={{
|
<span style={{
|
||||||
whiteSpace: "nowrap",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
}}>
|
whiteSpace: "nowrap",
|
||||||
{enabledActions.length === 0 ? "None" : enabledActionsList}
|
flex: 1,
|
||||||
|
minWidth: 0
|
||||||
|
}}>
|
||||||
|
{enabledActionsList || "None"}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
|
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
|
||||||
style={{
|
style={{
|
||||||
// fontSize: "14px",
|
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
marginLeft: isExpanded ? "2px" : "-2px",
|
marginLeft: isExpanded ? "2px" : "-2px",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CollapsibleSection>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div style={{ padding: "0" }}>
|
<div style={{ padding: "0" }}>
|
||||||
@@ -129,13 +171,17 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
}}>
|
}}>
|
||||||
Auto-approve allows Cline to perform actions without asking for permission. Only enable for
|
Auto-approve allows Cline to perform actions without asking for permission. Only enable for
|
||||||
actions you fully trust, and consider setting a low request limit as a safeguard.
|
actions you fully trust.
|
||||||
</div>
|
</div>
|
||||||
{actions.map((action) => (
|
{actions.map((action) => (
|
||||||
<div key={action.id} style={{ margin: "6px 0" }}>
|
<div key={action.id} style={{ margin: "6px 0" }}>
|
||||||
<VSCodeCheckbox checked={action.enabled} onChange={() => toggleAction(action.id)}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
{action.label}
|
<VSCodeCheckbox
|
||||||
</VSCodeCheckbox>
|
checked={action.enabled}
|
||||||
|
onChange={actionHandlers[action.id]}>
|
||||||
|
{action.label}
|
||||||
|
</VSCodeCheckbox>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginLeft: "28px",
|
marginLeft: "28px",
|
||||||
@@ -146,76 +192,10 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: "0.5px",
|
|
||||||
background: "var(--vscode-titleBar-inactiveForeground)",
|
|
||||||
margin: "15px 0",
|
|
||||||
opacity: 0.2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "8px",
|
|
||||||
marginTop: "10px",
|
|
||||||
marginBottom: "8px",
|
|
||||||
color: "var(--vscode-foreground)",
|
|
||||||
}}>
|
|
||||||
<span style={{ flexShrink: 1, minWidth: 0 }}>Max Requests:</span>
|
|
||||||
<VSCodeTextField
|
|
||||||
value={maxRequests.toString()}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = parseInt((e.target as HTMLInputElement).value)
|
|
||||||
if (!isNaN(value) && value > 0) {
|
|
||||||
setMaxRequests(value)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
color: "var(--vscode-descriptionForeground)",
|
|
||||||
fontSize: "12px",
|
|
||||||
marginBottom: "10px",
|
|
||||||
}}>
|
|
||||||
Cline will make this many API requests before asking for approval to proceed with the task.
|
|
||||||
</div>
|
|
||||||
<div style={{ margin: "6px 0" }}>
|
|
||||||
<VSCodeCheckbox
|
|
||||||
checked={enableNotifications}
|
|
||||||
onChange={() => setEnableNotifications((prev) => !prev)}>
|
|
||||||
Enable Notifications
|
|
||||||
</VSCodeCheckbox>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginLeft: "28px",
|
|
||||||
color: "var(--vscode-descriptionForeground)",
|
|
||||||
fontSize: "12px",
|
|
||||||
}}>
|
|
||||||
Receive system notifications when Cline requires approval to proceed or when a task is
|
|
||||||
completed.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollapsibleSection = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
color: var(--vscode-descriptionForeground);
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--vscode-foreground);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default AutoApproveMenu
|
export default AutoApproveMenu
|
||||||
|
|||||||
@@ -527,7 +527,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "8px",
|
gap: "8px",
|
||||||
backgroundColor: "var(--vscode-input-background)",
|
backgroundColor: "var(--vscode-input-background)",
|
||||||
minHeight: "100px",
|
|
||||||
margin: "10px 15px",
|
margin: "10px 15px",
|
||||||
padding: "8px"
|
padding: "8px"
|
||||||
}}
|
}}
|
||||||
@@ -652,7 +651,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
onHeightChange?.(height)
|
onHeightChange?.(height)
|
||||||
}}
|
}}
|
||||||
placeholder={placeholderText}
|
placeholder={placeholderText}
|
||||||
minRows={4}
|
minRows={2}
|
||||||
maxRows={20}
|
maxRows={20}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ interface ChatViewProps {
|
|||||||
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||||
|
|
||||||
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
|
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
|
||||||
const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, allowedCommands, writeDelayMs, mode, setMode } = useExtensionState()
|
const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, allowedCommands, writeDelayMs, mode, setMode, autoApprovalEnabled } = useExtensionState()
|
||||||
|
|
||||||
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
|
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
|
||||||
const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
|
const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
|
||||||
@@ -529,7 +529,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
|
|
||||||
const isAutoApproved = useCallback(
|
const isAutoApproved = useCallback(
|
||||||
(message: ClineMessage | undefined) => {
|
(message: ClineMessage | undefined) => {
|
||||||
if (!message || message.type !== "ask") return false
|
if (!autoApprovalEnabled || !message || message.type !== "ask") return false
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(alwaysAllowBrowser && message.ask === "browser_action_launch") ||
|
(alwaysAllowBrowser && message.ask === "browser_action_launch") ||
|
||||||
@@ -539,17 +539,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
(alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message))
|
(alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[
|
[autoApprovalEnabled, alwaysAllowBrowser, alwaysAllowReadOnly, isReadOnlyToolAction, alwaysAllowWrite, isWriteToolAction, alwaysAllowExecute, isAllowedCommand, alwaysAllowMcp, isMcpToolAlwaysAllowed]
|
||||||
alwaysAllowBrowser,
|
|
||||||
alwaysAllowReadOnly,
|
|
||||||
alwaysAllowWrite,
|
|
||||||
alwaysAllowExecute,
|
|
||||||
alwaysAllowMcp,
|
|
||||||
isReadOnlyToolAction,
|
|
||||||
isWriteToolAction,
|
|
||||||
isAllowedCommand,
|
|
||||||
isMcpToolAlwaysAllowed
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import { render, fireEvent, screen } from "@testing-library/react"
|
||||||
|
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
||||||
|
import AutoApproveMenu from "../AutoApproveMenu"
|
||||||
|
import { codeMode, defaultPrompts } from "../../../../../src/shared/modes"
|
||||||
|
|
||||||
|
// Mock the ExtensionStateContext hook
|
||||||
|
jest.mock("../../../context/ExtensionStateContext")
|
||||||
|
|
||||||
|
const mockUseExtensionState = useExtensionState as jest.MockedFunction<typeof useExtensionState>
|
||||||
|
|
||||||
|
describe("AutoApproveMenu", () => {
|
||||||
|
const defaultMockState = {
|
||||||
|
// Required state properties
|
||||||
|
version: "1.0.0",
|
||||||
|
clineMessages: [],
|
||||||
|
taskHistory: [],
|
||||||
|
shouldShowAnnouncement: false,
|
||||||
|
allowedCommands: [],
|
||||||
|
soundEnabled: false,
|
||||||
|
soundVolume: 0.5,
|
||||||
|
diffEnabled: false,
|
||||||
|
fuzzyMatchThreshold: 1.0,
|
||||||
|
preferredLanguage: "English",
|
||||||
|
writeDelayMs: 1000,
|
||||||
|
browserViewportSize: "900x600",
|
||||||
|
screenshotQuality: 75,
|
||||||
|
terminalOutputLineLimit: 500,
|
||||||
|
mcpEnabled: true,
|
||||||
|
requestDelaySeconds: 5,
|
||||||
|
currentApiConfigName: "default",
|
||||||
|
listApiConfigMeta: [],
|
||||||
|
mode: codeMode,
|
||||||
|
customPrompts: defaultPrompts,
|
||||||
|
enhancementApiConfigId: "",
|
||||||
|
didHydrateState: true,
|
||||||
|
showWelcome: false,
|
||||||
|
theme: {},
|
||||||
|
glamaModels: {},
|
||||||
|
openRouterModels: {},
|
||||||
|
openAiModels: [],
|
||||||
|
mcpServers: [],
|
||||||
|
filePaths: [],
|
||||||
|
|
||||||
|
// Auto-approve specific properties
|
||||||
|
alwaysAllowReadOnly: false,
|
||||||
|
alwaysAllowWrite: false,
|
||||||
|
alwaysAllowExecute: false,
|
||||||
|
alwaysAllowBrowser: false,
|
||||||
|
alwaysAllowMcp: false,
|
||||||
|
alwaysApproveResubmit: false,
|
||||||
|
autoApprovalEnabled: false,
|
||||||
|
|
||||||
|
// Required setter functions
|
||||||
|
setApiConfiguration: jest.fn(),
|
||||||
|
setCustomInstructions: jest.fn(),
|
||||||
|
setAlwaysAllowReadOnly: jest.fn(),
|
||||||
|
setAlwaysAllowWrite: jest.fn(),
|
||||||
|
setAlwaysAllowExecute: jest.fn(),
|
||||||
|
setAlwaysAllowBrowser: jest.fn(),
|
||||||
|
setAlwaysAllowMcp: jest.fn(),
|
||||||
|
setShowAnnouncement: jest.fn(),
|
||||||
|
setAllowedCommands: jest.fn(),
|
||||||
|
setSoundEnabled: jest.fn(),
|
||||||
|
setSoundVolume: jest.fn(),
|
||||||
|
setDiffEnabled: jest.fn(),
|
||||||
|
setBrowserViewportSize: jest.fn(),
|
||||||
|
setFuzzyMatchThreshold: jest.fn(),
|
||||||
|
setPreferredLanguage: jest.fn(),
|
||||||
|
setWriteDelayMs: jest.fn(),
|
||||||
|
setScreenshotQuality: jest.fn(),
|
||||||
|
setTerminalOutputLineLimit: jest.fn(),
|
||||||
|
setMcpEnabled: jest.fn(),
|
||||||
|
setAlwaysApproveResubmit: jest.fn(),
|
||||||
|
setRequestDelaySeconds: jest.fn(),
|
||||||
|
setCurrentApiConfigName: jest.fn(),
|
||||||
|
setListApiConfigMeta: jest.fn(),
|
||||||
|
onUpdateApiConfig: jest.fn(),
|
||||||
|
setMode: jest.fn(),
|
||||||
|
setCustomPrompts: jest.fn(),
|
||||||
|
setEnhancementApiConfigId: jest.fn(),
|
||||||
|
setAutoApprovalEnabled: jest.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockUseExtensionState.mockReturnValue(defaultMockState)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders with initial collapsed state", () => {
|
||||||
|
render(<AutoApproveMenu />)
|
||||||
|
|
||||||
|
// Check for main checkbox and label
|
||||||
|
expect(screen.getByText("Auto-approve:")).toBeInTheDocument()
|
||||||
|
expect(screen.getByText("None")).toBeInTheDocument()
|
||||||
|
|
||||||
|
// Verify the menu is collapsed (actions not visible)
|
||||||
|
expect(screen.queryByText("Read files and directories")).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("expands menu when clicked", () => {
|
||||||
|
render(<AutoApproveMenu />)
|
||||||
|
|
||||||
|
// Click to expand
|
||||||
|
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||||
|
|
||||||
|
// Verify menu items are visible
|
||||||
|
expect(screen.getByText("Read files and directories")).toBeInTheDocument()
|
||||||
|
expect(screen.getByText("Edit files")).toBeInTheDocument()
|
||||||
|
expect(screen.getByText("Execute safe commands")).toBeInTheDocument()
|
||||||
|
expect(screen.getByText("Use the browser")).toBeInTheDocument()
|
||||||
|
expect(screen.getByText("Use MCP servers")).toBeInTheDocument()
|
||||||
|
expect(screen.getByText("Retry failed requests")).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("toggles main auto-approval checkbox", () => {
|
||||||
|
render(<AutoApproveMenu />)
|
||||||
|
|
||||||
|
const mainCheckbox = screen.getByRole("checkbox")
|
||||||
|
fireEvent.click(mainCheckbox)
|
||||||
|
|
||||||
|
expect(defaultMockState.setAutoApprovalEnabled).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("toggles individual permissions", () => {
|
||||||
|
render(<AutoApproveMenu />)
|
||||||
|
|
||||||
|
// Expand menu
|
||||||
|
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||||
|
|
||||||
|
// Click read files checkbox
|
||||||
|
fireEvent.click(screen.getByText("Read files and directories"))
|
||||||
|
expect(defaultMockState.setAlwaysAllowReadOnly).toHaveBeenCalledWith(true)
|
||||||
|
|
||||||
|
// Click edit files checkbox
|
||||||
|
fireEvent.click(screen.getByText("Edit files"))
|
||||||
|
expect(defaultMockState.setAlwaysAllowWrite).toHaveBeenCalledWith(true)
|
||||||
|
|
||||||
|
// Click execute commands checkbox
|
||||||
|
fireEvent.click(screen.getByText("Execute safe commands"))
|
||||||
|
expect(defaultMockState.setAlwaysAllowExecute).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("displays enabled actions in summary", () => {
|
||||||
|
mockUseExtensionState.mockReturnValue({
|
||||||
|
...defaultMockState,
|
||||||
|
alwaysAllowReadOnly: true,
|
||||||
|
alwaysAllowWrite: true,
|
||||||
|
autoApprovalEnabled: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<AutoApproveMenu />)
|
||||||
|
|
||||||
|
// Check that enabled actions are shown in summary
|
||||||
|
expect(screen.getByText("Read, Edit")).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("preserves checkbox states", () => {
|
||||||
|
// Mock state with some permissions enabled
|
||||||
|
const mockState = {
|
||||||
|
...defaultMockState,
|
||||||
|
alwaysAllowReadOnly: true,
|
||||||
|
alwaysAllowWrite: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update mock to return our state
|
||||||
|
mockUseExtensionState.mockReturnValue(mockState)
|
||||||
|
|
||||||
|
render(<AutoApproveMenu />)
|
||||||
|
|
||||||
|
// Expand menu
|
||||||
|
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||||
|
|
||||||
|
// Verify read and edit checkboxes are checked
|
||||||
|
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
|
||||||
|
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
|
||||||
|
|
||||||
|
// Verify the setters haven't been called yet
|
||||||
|
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
|
||||||
|
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Collapse menu
|
||||||
|
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||||
|
|
||||||
|
// Expand again
|
||||||
|
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||||
|
|
||||||
|
// Verify checkboxes are still present
|
||||||
|
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
|
||||||
|
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
|
||||||
|
|
||||||
|
// Verify the setters still haven't been called
|
||||||
|
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
|
||||||
|
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, waitFor } from '@testing-library/react'
|
||||||
|
import ChatView from '../ChatView'
|
||||||
|
import { ExtensionStateContextProvider } from '../../../context/ExtensionStateContext'
|
||||||
|
import { vscode } from '../../../utils/vscode'
|
||||||
|
|
||||||
|
// Mock vscode API
|
||||||
|
jest.mock('../../../utils/vscode', () => ({
|
||||||
|
vscode: {
|
||||||
|
postMessage: jest.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock all problematic dependencies
|
||||||
|
jest.mock('rehype-highlight', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => () => {},
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('hast-util-to-text', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => '',
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock components that use ESM dependencies
|
||||||
|
jest.mock('../BrowserSessionRow', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: function MockBrowserSessionRow({ messages }: { messages: any[] }) {
|
||||||
|
return <div data-testid="browser-session">{JSON.stringify(messages)}</div>
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../ChatRow', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: function MockChatRow({ message }: { message: any }) {
|
||||||
|
return <div data-testid="chat-row">{JSON.stringify(message)}</div>
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../TaskHeader', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: function MockTaskHeader({ task }: { task: any }) {
|
||||||
|
return <div data-testid="task-header">{JSON.stringify(task)}</div>
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../AutoApproveMenu', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../../common/CodeBlock', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => null,
|
||||||
|
CODE_BLOCK_BG_COLOR: 'rgb(30, 30, 30)',
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../../common/CodeAccordian', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../ContextMenu', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock window.postMessage to trigger state hydration
|
||||||
|
const mockPostMessage = (state: any) => {
|
||||||
|
window.postMessage({
|
||||||
|
type: 'state',
|
||||||
|
state: {
|
||||||
|
version: '1.0.0',
|
||||||
|
clineMessages: [],
|
||||||
|
taskHistory: [],
|
||||||
|
shouldShowAnnouncement: false,
|
||||||
|
allowedCommands: [],
|
||||||
|
alwaysAllowExecute: false,
|
||||||
|
autoApprovalEnabled: true,
|
||||||
|
...state
|
||||||
|
}
|
||||||
|
}, '*')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ChatView - Auto Approval Tests', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('auto-approves read operations when enabled', async () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// First hydrate state with initial task
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowReadOnly: true,
|
||||||
|
autoApprovalEnabled: true,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then send the read tool ask message
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowReadOnly: true,
|
||||||
|
autoApprovalEnabled: true,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ask',
|
||||||
|
ask: 'tool',
|
||||||
|
ts: Date.now(),
|
||||||
|
text: JSON.stringify({ tool: 'readFile', path: 'test.txt' }),
|
||||||
|
partial: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the auto-approval message
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'askResponse',
|
||||||
|
askResponse: 'yesButtonClicked'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not auto-approve when autoApprovalEnabled is false', async () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// First hydrate state with initial task
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowReadOnly: true,
|
||||||
|
autoApprovalEnabled: false,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then send the read tool ask message
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowReadOnly: true,
|
||||||
|
autoApprovalEnabled: false,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ask',
|
||||||
|
ask: 'tool',
|
||||||
|
ts: Date.now(),
|
||||||
|
text: JSON.stringify({ tool: 'readFile', path: 'test.txt' }),
|
||||||
|
partial: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify no auto-approval message was sent
|
||||||
|
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||||
|
type: 'askResponse',
|
||||||
|
askResponse: 'yesButtonClicked'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('auto-approves write operations when enabled', async () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// First hydrate state with initial task
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowWrite: true,
|
||||||
|
autoApprovalEnabled: true,
|
||||||
|
writeDelayMs: 0,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then send the write tool ask message
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowWrite: true,
|
||||||
|
autoApprovalEnabled: true,
|
||||||
|
writeDelayMs: 0,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ask',
|
||||||
|
ask: 'tool',
|
||||||
|
ts: Date.now(),
|
||||||
|
text: JSON.stringify({ tool: 'editedExistingFile', path: 'test.txt' }),
|
||||||
|
partial: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the auto-approval message
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'askResponse',
|
||||||
|
askResponse: 'yesButtonClicked'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('auto-approves browser actions when enabled', async () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// First hydrate state with initial task
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowBrowser: true,
|
||||||
|
autoApprovalEnabled: true,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then send the browser action ask message
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowBrowser: true,
|
||||||
|
autoApprovalEnabled: true,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ask',
|
||||||
|
ask: 'browser_action_launch',
|
||||||
|
ts: Date.now(),
|
||||||
|
text: JSON.stringify({ action: 'launch', url: 'http://example.com' }),
|
||||||
|
partial: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the auto-approval message
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'askResponse',
|
||||||
|
askResponse: 'yesButtonClicked'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -46,6 +46,11 @@ jest.mock('../ChatRow', () => ({
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
jest.mock('../AutoApproveMenu', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
interface ChatTextAreaProps {
|
interface ChatTextAreaProps {
|
||||||
onSend: (value: string) => void;
|
onSend: (value: string) => void;
|
||||||
inputValue?: string;
|
inputValue?: string;
|
||||||
@@ -139,6 +144,187 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('defaults autoApprovalEnabled to true if any individual auto-approval flags are true', async () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test cases with different individual flags
|
||||||
|
const testCases = [
|
||||||
|
{ alwaysAllowBrowser: true },
|
||||||
|
{ alwaysAllowReadOnly: true },
|
||||||
|
{ alwaysAllowWrite: true },
|
||||||
|
{ alwaysAllowExecute: true },
|
||||||
|
{ alwaysAllowMcp: true }
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const flags of testCases) {
|
||||||
|
// Reset state
|
||||||
|
mockPostMessage({
|
||||||
|
...flags,
|
||||||
|
clineMessages: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send an action that should be auto-approved
|
||||||
|
mockPostMessage({
|
||||||
|
...flags,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ask',
|
||||||
|
ask: flags.alwaysAllowBrowser ? 'browser_action_launch' :
|
||||||
|
flags.alwaysAllowReadOnly ? 'tool' :
|
||||||
|
flags.alwaysAllowWrite ? 'tool' :
|
||||||
|
flags.alwaysAllowExecute ? 'command' :
|
||||||
|
'use_mcp_server',
|
||||||
|
ts: Date.now(),
|
||||||
|
text: flags.alwaysAllowBrowser ? JSON.stringify({ action: 'launch', url: 'http://example.com' }) :
|
||||||
|
flags.alwaysAllowReadOnly ? JSON.stringify({ tool: 'readFile', path: 'test.txt' }) :
|
||||||
|
flags.alwaysAllowWrite ? JSON.stringify({ tool: 'editedExistingFile', path: 'test.txt' }) :
|
||||||
|
flags.alwaysAllowExecute ? 'npm test' :
|
||||||
|
JSON.stringify({ type: 'use_mcp_tool', serverName: 'test', toolName: 'test' }),
|
||||||
|
partial: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for auto-approval
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'askResponse',
|
||||||
|
askResponse: 'yesButtonClicked'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no auto-approval when no flags are true
|
||||||
|
jest.clearAllMocks()
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowBrowser: false,
|
||||||
|
alwaysAllowReadOnly: false,
|
||||||
|
alwaysAllowWrite: false,
|
||||||
|
alwaysAllowExecute: false,
|
||||||
|
alwaysAllowMcp: false,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ask',
|
||||||
|
ask: 'browser_action_launch',
|
||||||
|
ts: Date.now(),
|
||||||
|
text: JSON.stringify({ action: 'launch', url: 'http://example.com' }),
|
||||||
|
partial: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait a bit to ensure no auto-approval happens
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||||
|
type: 'askResponse',
|
||||||
|
askResponse: 'yesButtonClicked'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not auto-approve any actions when autoApprovalEnabled is false', () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// First hydrate state with initial task
|
||||||
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: false,
|
||||||
|
alwaysAllowBrowser: true,
|
||||||
|
alwaysAllowReadOnly: true,
|
||||||
|
alwaysAllowWrite: true,
|
||||||
|
alwaysAllowExecute: true,
|
||||||
|
allowedCommands: ['npm test'],
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test various types of actions that should not be auto-approved
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
ask: 'browser_action_launch',
|
||||||
|
text: JSON.stringify({ action: 'launch', url: 'http://example.com' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ask: 'tool',
|
||||||
|
text: JSON.stringify({ tool: 'readFile', path: 'test.txt' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ask: 'tool',
|
||||||
|
text: JSON.stringify({ tool: 'editedExistingFile', path: 'test.txt' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ask: 'command',
|
||||||
|
text: 'npm test'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
testCases.forEach(testCase => {
|
||||||
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: false,
|
||||||
|
alwaysAllowBrowser: true,
|
||||||
|
alwaysAllowReadOnly: true,
|
||||||
|
alwaysAllowWrite: true,
|
||||||
|
alwaysAllowExecute: true,
|
||||||
|
allowedCommands: ['npm test'],
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ask',
|
||||||
|
ask: testCase.ask,
|
||||||
|
ts: Date.now(),
|
||||||
|
text: testCase.text,
|
||||||
|
partial: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify no auto-approval message was sent
|
||||||
|
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||||
|
type: 'askResponse',
|
||||||
|
askResponse: 'yesButtonClicked'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('auto-approves browser actions when alwaysAllowBrowser is enabled', async () => {
|
it('auto-approves browser actions when alwaysAllowBrowser is enabled', async () => {
|
||||||
render(
|
render(
|
||||||
<ExtensionStateContextProvider>
|
<ExtensionStateContextProvider>
|
||||||
@@ -153,6 +339,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// First hydrate state with initial task
|
// First hydrate state with initial task
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowBrowser: true,
|
alwaysAllowBrowser: true,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
{
|
{
|
||||||
@@ -166,6 +353,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// Then send the browser action ask message
|
// Then send the browser action ask message
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowBrowser: true,
|
alwaysAllowBrowser: true,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
{
|
{
|
||||||
@@ -207,6 +395,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// First hydrate state with initial task
|
// First hydrate state with initial task
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowReadOnly: true,
|
alwaysAllowReadOnly: true,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
{
|
{
|
||||||
@@ -220,6 +409,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// Then send the read-only tool ask message
|
// Then send the read-only tool ask message
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowReadOnly: true,
|
alwaysAllowReadOnly: true,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
{
|
{
|
||||||
@@ -262,6 +452,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// First hydrate state with initial task
|
// First hydrate state with initial task
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowWrite: true,
|
alwaysAllowWrite: true,
|
||||||
writeDelayMs: 0,
|
writeDelayMs: 0,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -276,6 +467,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// Then send the write tool ask message
|
// Then send the write tool ask message
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowWrite: true,
|
alwaysAllowWrite: true,
|
||||||
writeDelayMs: 0,
|
writeDelayMs: 0,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -318,6 +510,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// First hydrate state with initial task
|
// First hydrate state with initial task
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowWrite: true,
|
alwaysAllowWrite: true,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
{
|
{
|
||||||
@@ -331,6 +524,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// Then send a non-tool write operation message
|
// Then send a non-tool write operation message
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowWrite: true,
|
alwaysAllowWrite: true,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
{
|
{
|
||||||
@@ -371,6 +565,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// First hydrate state with initial task
|
// First hydrate state with initial task
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowExecute: true,
|
alwaysAllowExecute: true,
|
||||||
allowedCommands: ['npm test'],
|
allowedCommands: ['npm test'],
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -385,6 +580,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// Then send the command ask message
|
// Then send the command ask message
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowExecute: true,
|
alwaysAllowExecute: true,
|
||||||
allowedCommands: ['npm test'],
|
allowedCommands: ['npm test'],
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -427,6 +623,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// First hydrate state with initial task
|
// First hydrate state with initial task
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowExecute: true,
|
alwaysAllowExecute: true,
|
||||||
allowedCommands: ['npm test'],
|
allowedCommands: ['npm test'],
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -441,6 +638,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// Then send the disallowed command ask message
|
// Then send the disallowed command ask message
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowExecute: true,
|
alwaysAllowExecute: true,
|
||||||
allowedCommands: ['npm test'],
|
allowedCommands: ['npm test'],
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -498,6 +696,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// First hydrate state with initial task
|
// First hydrate state with initial task
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowExecute: true,
|
alwaysAllowExecute: true,
|
||||||
allowedCommands: ['npm test', 'npm run build', 'echo', 'Select-String'],
|
allowedCommands: ['npm test', 'npm run build', 'echo', 'Select-String'],
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -512,6 +711,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// Then send the chained command ask message
|
// Then send the chained command ask message
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowExecute: true,
|
alwaysAllowExecute: true,
|
||||||
allowedCommands: ['npm test', 'npm run build', 'echo', 'Select-String'],
|
allowedCommands: ['npm test', 'npm run build', 'echo', 'Select-String'],
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -585,6 +785,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
|
|
||||||
// Then send the chained command ask message
|
// Then send the chained command ask message
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowExecute: true,
|
alwaysAllowExecute: true,
|
||||||
allowedCommands: ['npm test', 'Select-String'],
|
allowedCommands: ['npm test', 'Select-String'],
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -643,6 +844,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowExecute: true,
|
alwaysAllowExecute: true,
|
||||||
allowedCommands: ['npm test', 'Select-String'],
|
allowedCommands: ['npm test', 'Select-String'],
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -656,6 +858,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowExecute: true,
|
alwaysAllowExecute: true,
|
||||||
allowedCommands: ['npm test', 'Select-String'],
|
allowedCommands: ['npm test', 'Select-String'],
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -688,6 +891,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowExecute: true,
|
alwaysAllowExecute: true,
|
||||||
allowedCommands: ['npm test', 'Select-String'],
|
allowedCommands: ['npm test', 'Select-String'],
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -701,6 +905,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowExecute: true,
|
alwaysAllowExecute: true,
|
||||||
allowedCommands: ['npm test', 'Select-String'],
|
allowedCommands: ['npm test', 'Select-String'],
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
@@ -748,6 +953,7 @@ describe('ChatView - Sound Playing Tests', () => {
|
|||||||
|
|
||||||
// First hydrate state with initial task and streaming
|
// First hydrate state with initial task and streaming
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowBrowser: true,
|
alwaysAllowBrowser: true,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
{
|
{
|
||||||
@@ -768,6 +974,7 @@ describe('ChatView - Sound Playing Tests', () => {
|
|||||||
|
|
||||||
// Then send the browser action ask message (streaming finished)
|
// Then send the browser action ask message (streaming finished)
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowBrowser: true,
|
alwaysAllowBrowser: true,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
{
|
{
|
||||||
@@ -807,6 +1014,7 @@ describe('ChatView - Sound Playing Tests', () => {
|
|||||||
|
|
||||||
// First hydrate state with initial task and streaming
|
// First hydrate state with initial task and streaming
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowBrowser: false,
|
alwaysAllowBrowser: false,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
{
|
{
|
||||||
@@ -827,6 +1035,7 @@ describe('ChatView - Sound Playing Tests', () => {
|
|||||||
|
|
||||||
// Then send the browser action ask message (streaming finished)
|
// Then send the browser action ask message (streaming finished)
|
||||||
mockPostMessage({
|
mockPostMessage({
|
||||||
|
autoApprovalEnabled: true,
|
||||||
alwaysAllowBrowser: false,
|
alwaysAllowBrowser: false,
|
||||||
clineMessages: [
|
clineMessages: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||||||
setCustomPrompts: (value: CustomPrompts) => void
|
setCustomPrompts: (value: CustomPrompts) => void
|
||||||
enhancementApiConfigId?: string
|
enhancementApiConfigId?: string
|
||||||
setEnhancementApiConfigId: (value: string) => void
|
setEnhancementApiConfigId: (value: string) => void
|
||||||
|
autoApprovalEnabled?: boolean
|
||||||
|
setAutoApprovalEnabled: (value: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||||
@@ -121,11 +123,22 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
const message: ExtensionMessage = event.data
|
const message: ExtensionMessage = event.data
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "state": {
|
case "state": {
|
||||||
|
const newState = message.state!
|
||||||
|
// Set autoApprovalEnabled to true if undefined and any individual flag is true
|
||||||
|
if (newState.autoApprovalEnabled === undefined) {
|
||||||
|
newState.autoApprovalEnabled = !!(
|
||||||
|
newState.alwaysAllowBrowser ||
|
||||||
|
newState.alwaysAllowReadOnly ||
|
||||||
|
newState.alwaysAllowWrite ||
|
||||||
|
newState.alwaysAllowExecute ||
|
||||||
|
newState.alwaysAllowMcp ||
|
||||||
|
newState.alwaysApproveResubmit)
|
||||||
|
}
|
||||||
setState(prevState => ({
|
setState(prevState => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
...message.state!
|
...newState
|
||||||
}))
|
}))
|
||||||
const config = message.state?.apiConfiguration
|
const config = newState.apiConfiguration
|
||||||
const hasKey = checkExistKey(config)
|
const hasKey = checkExistKey(config)
|
||||||
setShowWelcome(!hasKey)
|
setShowWelcome(!hasKey)
|
||||||
setDidHydrateState(true)
|
setDidHydrateState(true)
|
||||||
@@ -237,6 +250,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
|
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
|
||||||
setCustomPrompts: (value) => setState((prevState) => ({ ...prevState, customPrompts: value })),
|
setCustomPrompts: (value) => setState((prevState) => ({ ...prevState, customPrompts: value })),
|
||||||
setEnhancementApiConfigId: (value) => setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
|
setEnhancementApiConfigId: (value) => setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
|
||||||
|
setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||||
|
|||||||
Reference in New Issue
Block a user