mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Simplify auto-approving code and make it work better with browser actions (#21)
This commit is contained in:
@@ -34,8 +34,18 @@ interface ChatViewProps {
|
||||
|
||||
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||
|
||||
const ALLOWED_AUTO_EXECUTE_COMMANDS = [
|
||||
'npm',
|
||||
'npx',
|
||||
'tsc',
|
||||
'git log',
|
||||
'git diff',
|
||||
'git show',
|
||||
'ls'
|
||||
] as const
|
||||
|
||||
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
|
||||
const { version, clineMessages: messages, taskHistory, apiConfiguration } = useExtensionState()
|
||||
const { version, clineMessages: messages, taskHistory, apiConfiguration, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute } = useExtensionState()
|
||||
|
||||
//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)
|
||||
@@ -675,6 +685,60 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
[expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Only proceed if we have an ask and buttons are enabled
|
||||
if (!clineAsk || !enableButtons) return
|
||||
|
||||
const isReadOnlyToolAction = () => {
|
||||
const lastMessage = messages.at(-1)
|
||||
if (lastMessage?.type === "ask" && lastMessage.text) {
|
||||
const tool = JSON.parse(lastMessage.text)
|
||||
return ["readFile", "listFiles", "searchFiles"].includes(tool.tool)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const isWriteToolAction = () => {
|
||||
const lastMessage = messages.at(-1)
|
||||
if (lastMessage?.type === "ask" && lastMessage.text) {
|
||||
const tool = JSON.parse(lastMessage.text)
|
||||
return ["editedExistingFile", "newFileCreated"].includes(tool.tool)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const isAllowedCommand = () => {
|
||||
const lastMessage = messages.at(-1)
|
||||
if (lastMessage?.type === "ask" && lastMessage.text) {
|
||||
const command = lastMessage.text
|
||||
|
||||
// Check for command chaining characters
|
||||
if (command.includes('&&') ||
|
||||
command.includes(';') ||
|
||||
command.includes('||') ||
|
||||
command.includes('|') ||
|
||||
command.includes('$(') ||
|
||||
command.includes('`')) {
|
||||
return false
|
||||
}
|
||||
const trimmedCommand = command.trim().toLowerCase()
|
||||
return ALLOWED_AUTO_EXECUTE_COMMANDS.some(prefix =>
|
||||
trimmedCommand.startsWith(prefix.toLowerCase())
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
(alwaysAllowBrowser && clineAsk === "browser_action_launch") ||
|
||||
(alwaysAllowReadOnly && clineAsk === "tool" && isReadOnlyToolAction()) ||
|
||||
(alwaysAllowWrite && clineAsk === "tool" && isWriteToolAction()) ||
|
||||
(alwaysAllowExecute && clineAsk === "command" && isAllowedCommand())
|
||||
) {
|
||||
handlePrimaryButtonClick()
|
||||
}
|
||||
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, messages])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -38,7 +38,6 @@ jest.mock('../ChatRow', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
// Mock Virtuoso component
|
||||
jest.mock('react-virtuoso', () => ({
|
||||
Virtuoso: ({ children }: any) => (
|
||||
@@ -74,6 +73,7 @@ describe('ChatView', () => {
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
alwaysAllowBrowser: true,
|
||||
openRouterModels: {},
|
||||
didHydrateState: true,
|
||||
showWelcome: false,
|
||||
@@ -82,13 +82,14 @@ describe('ChatView', () => {
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
uriScheme: 'vscode',
|
||||
|
||||
|
||||
setApiConfiguration: jest.fn(),
|
||||
setShowAnnouncement: jest.fn(),
|
||||
setCustomInstructions: jest.fn(),
|
||||
setAlwaysAllowReadOnly: jest.fn(),
|
||||
setAlwaysAllowWrite: jest.fn(),
|
||||
setCustomInstructions: jest.fn(),
|
||||
setAlwaysAllowExecute: jest.fn(),
|
||||
setApiConfiguration: jest.fn(),
|
||||
setShowAnnouncement: jest.fn()
|
||||
setAlwaysAllowBrowser: jest.fn()
|
||||
}
|
||||
|
||||
// Mock the useExtensionState hook
|
||||
@@ -106,6 +107,118 @@ describe('ChatView', () => {
|
||||
)
|
||||
}
|
||||
|
||||
describe('Always Allow Logic', () => {
|
||||
it('should auto-approve read-only tool actions when alwaysAllowReadOnly is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
text: JSON.stringify({ tool: 'readFile' }),
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
|
||||
it('should auto-approve write tool actions when alwaysAllowWrite is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
text: JSON.stringify({ tool: 'editedExistingFile' }),
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
|
||||
it('should auto-approve allowed execute commands when alwaysAllowExecute is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'command',
|
||||
text: 'npm install',
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
|
||||
it('should not auto-approve disallowed execute commands even when alwaysAllowExecute is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'command',
|
||||
text: 'rm -rf /',
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not auto-approve commands with chaining characters when alwaysAllowExecute is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'command',
|
||||
text: 'npm install && rm -rf /',
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should auto-approve browser actions when alwaysAllowBrowser is true', () => {
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'browser_action_launch',
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
|
||||
it('should not auto-approve when corresponding alwaysAllow flag is false', () => {
|
||||
mockState.alwaysAllowReadOnly = false
|
||||
mockState.clineMessages = [
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
text: JSON.stringify({ tool: 'readFile' }),
|
||||
ts: Date.now(),
|
||||
}
|
||||
]
|
||||
renderChatView()
|
||||
|
||||
expect(vscode.postMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Streaming State', () => {
|
||||
it('should show cancel button while streaming and trigger cancel on click', async () => {
|
||||
mockState.clineMessages = [
|
||||
@@ -168,4 +281,4 @@ describe('ChatView', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user