Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6fda136
feat: Add terminal command permissions UI to chat interface (#5480)
hannesrudolph Jul 17, 2025
6fa0a52
fix: address PR review feedback
hannesrudolph Jul 17, 2025
dd533c2
refactor: convert showSuggestions from state to constant
hannesrudolph Jul 17, 2025
b24400b
fix: address PR review feedback
roomote Jul 21, 2025
860631c
fix: address code review feedback
roomote Jul 21, 2025
ebf1b24
fix: improve command parsing to handle Output: separator correctly
hannesrudolph Jul 22, 2025
f6642f6
fix: prevent command output from appearing in permissions UI
hannesrudolph Jul 22, 2025
0a25464
refactor: eliminate code redundancy between extractCommandPatterns an…
hannesrudolph Jul 23, 2025
4b257fc
refactor: simplify command pattern parser using shell-quote library
roomote Jul 23, 2025
38ed84b
fix: consolidate command parsing logic and integrate security warnings
hannesrudolph Jul 23, 2025
55d0ff3
chore: remove temporary review files
hannesrudolph Jul 23, 2025
17dbda3
fix: update command parser to handle all edge cases from original imp…
hannesrudolph Jul 23, 2025
0a5cf46
fix: revert conditional display of command pattern selector
hannesrudolph Jul 23, 2025
8b4150e
Refactor command execution and security handling
daniel-lxs Jul 24, 2025
627ea84
fix: stop extracting patterns at command flags and revert command-val…
daniel-lxs Jul 24, 2025
cbc756d
refactor: remove commandPatterns.ts and simplify command parsing
daniel-lxs Jul 24, 2025
7799f0c
refactor: remove automatic 'commands' description suffix from patterns
daniel-lxs Jul 24, 2025
9481c95
Update webview-ui/src/components/chat/CommandExecution.tsx
daniel-lxs Jul 24, 2025
63ce9e7
Update webview-ui/src/components/chat/CommandPatternSelector.tsx
daniel-lxs Jul 24, 2025
d819ae5
fix: remove unused index parameter in CommandPatternSelector
daniel-lxs Jul 24, 2025
ea36059
fix: improve error handling in extractPatternsFromCommand function
daniel-lxs Jul 24, 2025
8235029
feat: simplify command permissions UI to use full commands instead of…
daniel-lxs Jul 25, 2025
20f99a2
feat: improve command permissions UI with click-to-edit functionality
daniel-lxs Jul 25, 2025
851617c
feat: restore command pattern extraction with editable UI
daniel-lxs Jul 25, 2025
77bc9e6
fix: remove 'Full command' description and use correct translation keys
daniel-lxs Jul 25, 2025
19c2c49
refactor: remove unused token validation function and simplify token …
daniel-lxs Jul 25, 2025
a18be7d
fix: update tests to match new CommandPatternSelector interface
daniel-lxs Jul 25, 2025
c4f5c5f
refactor: restore parseCommandAndOutput function from main
daniel-lxs Jul 25, 2025
8362771
fix: ensure unique command patterns and trim whitespace in command ex…
daniel-lxs Jul 25, 2025
b3068fb
fix: implement breaking patterns in command extraction
daniel-lxs Jul 25, 2025
9a465bb
fix: trim whitespace when adding patterns in extractFromTokens function
daniel-lxs Jul 25, 2025
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
65 changes: 61 additions & 4 deletions webview-ui/src/components/chat/CommandExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@ import { CommandExecutionStatus, commandExecutionStatusSchema } from "@roo-code/

import { ExtensionMessage } from "@roo/ExtensionMessage"
import { safeJsonParse } from "@roo/safeJsonParse"

import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"

import { vscode } from "@src/utils/vscode"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { cn } from "@src/lib/utils"
import { Button } from "@src/components/ui"
import CodeBlock from "../common/CodeBlock"
import { CommandPatternSelector } from "./CommandPatternSelector"
import { extractPatternsFromCommand } from "../../utils/command-parser"

interface CommandPattern {
pattern: string
description?: string
}

interface CommandExecutionProps {
executionId: string
Expand All @@ -22,7 +30,13 @@ interface CommandExecutionProps {
}

export const CommandExecution = ({ executionId, text, icon, title }: CommandExecutionProps) => {
const { terminalShellIntegrationDisabled = false } = useExtensionState()
const {
terminalShellIntegrationDisabled = false,
allowedCommands = [],
deniedCommands = [],
setAllowedCommands,
setDeniedCommands,
} = useExtensionState()

const { command, output: parsedOutput } = useMemo(() => parseCommandAndOutput(text), [text])

Expand All @@ -37,6 +51,37 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
// streaming output (this is the case for running commands).
const output = streamingOutput || parsedOutput

// Extract command patterns from the actual command that was executed
const commandPatterns = useMemo<CommandPattern[]>(() => {
const extractedPatterns = extractPatternsFromCommand(command)
return extractedPatterns.map((pattern) => ({
pattern,
}))
}, [command])

// Handle pattern changes
const handleAllowPatternChange = (pattern: string) => {
const isAllowed = allowedCommands.includes(pattern)
const newAllowed = isAllowed ? allowedCommands.filter((p) => p !== pattern) : [...allowedCommands, pattern]
const newDenied = deniedCommands.filter((p) => p !== pattern)

setAllowedCommands(newAllowed)
setDeniedCommands(newDenied)
vscode.postMessage({ type: "allowedCommands", commands: newAllowed })
vscode.postMessage({ type: "deniedCommands", commands: newDenied })
}

const handleDenyPatternChange = (pattern: string) => {
const isDenied = deniedCommands.includes(pattern)
const newDenied = isDenied ? deniedCommands.filter((p) => p !== pattern) : [...deniedCommands, pattern]
const newAllowed = allowedCommands.filter((p) => p !== pattern)

setAllowedCommands(newAllowed)
setDeniedCommands(newDenied)
vscode.postMessage({ type: "allowedCommands", commands: newAllowed })
vscode.postMessage({ type: "deniedCommands", commands: newDenied })
}

const onMessage = useCallback(
(event: MessageEvent) => {
const message: ExtensionMessage = event.data
Expand Down Expand Up @@ -121,9 +166,21 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
</div>
</div>

<div className="w-full bg-vscode-editor-background border border-vscode-border rounded-xs p-2">
<CodeBlock source={command} language="shell" />
<OutputContainer isExpanded={isExpanded} output={output} />
<div className="w-full bg-vscode-editor-background border border-vscode-border rounded-xs">
<div className="p-2">
<CodeBlock source={command} language="shell" />
<OutputContainer isExpanded={isExpanded} output={output} />
</div>
{command && command.trim() && (
<CommandPatternSelector
command={command}
patterns={commandPatterns}
allowedCommands={allowedCommands}
deniedCommands={deniedCommands}
onAllowPatternChange={handleAllowPatternChange}
onDenyPatternChange={handleDenyPatternChange}
/>
)}
</div>
</>
)
Expand Down
183 changes: 183 additions & 0 deletions webview-ui/src/components/chat/CommandPatternSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import React, { useState, useMemo } from "react"
import { Check, ChevronDown, Info, X } from "lucide-react"
import { cn } from "../../lib/utils"
import { useTranslation, Trans } from "react-i18next"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { StandardTooltip } from "../ui/standard-tooltip"

interface CommandPattern {
pattern: string
description?: string
}

interface CommandPatternSelectorProps {
command: string
patterns: CommandPattern[]
allowedCommands: string[]
deniedCommands: string[]
onAllowPatternChange: (pattern: string) => void
onDenyPatternChange: (pattern: string) => void
}

export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({
command,
patterns,
allowedCommands,
deniedCommands,
onAllowPatternChange,
onDenyPatternChange,
}) => {
const { t } = useTranslation()
const [isExpanded, setIsExpanded] = useState(false)
const [editingStates, setEditingStates] = useState<Record<string, { isEditing: boolean; value: string }>>({})

// Create a combined list with full command first, then patterns
const allPatterns = useMemo(() => {
const fullCommandPattern: CommandPattern = { pattern: command }

// Create a set to track unique patterns we've already seen
const seenPatterns = new Set<string>()
seenPatterns.add(command) // Add the full command first

// Filter out any patterns that are duplicates or are the same as the full command
const uniquePatterns = patterns.filter((p) => {
if (seenPatterns.has(p.pattern)) {
return false
}
seenPatterns.add(p.pattern)
return true
})

return [fullCommandPattern, ...uniquePatterns]
}, [command, patterns])

const getPatternStatus = (pattern: string): "allowed" | "denied" | "none" => {
if (allowedCommands.includes(pattern)) return "allowed"
if (deniedCommands.includes(pattern)) return "denied"
return "none"
}

const getEditState = (pattern: string) => {
return editingStates[pattern] || { isEditing: false, value: pattern }
}

const setEditState = (pattern: string, isEditing: boolean, value?: string) => {
setEditingStates((prev) => ({
...prev,
[pattern]: { isEditing, value: value ?? pattern },
}))
}

return (
<div className="border-t border-vscode-panel-border bg-vscode-sideBar-background/30">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full px-3 py-2 flex items-center justify-between hover:bg-vscode-list-hoverBackground transition-colors">
<div className="flex items-center gap-2">
<ChevronDown
className={cn("size-4 transition-transform", {
"-rotate-90": !isExpanded,
})}
/>
<span className="text-sm font-medium">{t("chat:commandExecution.manageCommands")}</span>
<StandardTooltip
content={
<div className="max-w-xs">
<Trans
i18nKey="chat:commandExecution.commandManagementDescription"
components={{
settingsLink: (
<VSCodeLink
href="command:workbench.action.openSettings?%5B%22roo-code%22%5D"
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground"
/>
),
}}
/>
</div>
}>
<Info className="size-3.5 text-vscode-descriptionForeground" />
</StandardTooltip>
</div>
</button>

{isExpanded && (
<div className="px-3 pb-3 space-y-2">
{allPatterns.map((item) => {
const editState = getEditState(item.pattern)
const status = getPatternStatus(editState.value)

return (
<div key={item.pattern} className="ml-5 flex items-center gap-2">
<div className="flex-1">
{editState.isEditing ? (
<input
type="text"
value={editState.value}
onChange={(e) => setEditState(item.pattern, true, e.target.value)}
onBlur={() => setEditState(item.pattern, false)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setEditState(item.pattern, false)
}
if (e.key === "Escape") {
setEditState(item.pattern, false, item.pattern)
}
}}
className="font-mono text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded px-2 py-1.5 w-full focus:outline-0 focus:ring-1 focus:ring-vscode-focusBorder"
placeholder={item.pattern}
autoFocus
/>
) : (
<div
onClick={() => setEditState(item.pattern, true)}
className="font-mono text-xs text-vscode-foreground cursor-pointer hover:bg-vscode-list-hoverBackground px-2 py-1.5 rounded transition-colors border border-transparent"
title="Click to edit pattern">
<span>{editState.value}</span>
{item.description && (
<span className="text-vscode-descriptionForeground ml-2">
- {item.description}
</span>
)}
</div>
)}
</div>
<div className="flex items-center gap-1">
<button
className={cn("p-1 rounded transition-all", {
"bg-green-500/20 text-green-500 hover:bg-green-500/30":
status === "allowed",
"text-vscode-descriptionForeground hover:text-green-500 hover:bg-green-500/10":
status !== "allowed",
})}
onClick={() => onAllowPatternChange(editState.value)}
aria-label={t(
status === "allowed"
? "chat:commandExecution.removeFromAllowed"
: "chat:commandExecution.addToAllowed",
)}>
<Check className="size-3.5" />
</button>
<button
className={cn("p-1 rounded transition-all", {
"bg-red-500/20 text-red-500 hover:bg-red-500/30": status === "denied",
"text-vscode-descriptionForeground hover:text-red-500 hover:bg-red-500/10":
status !== "denied",
})}
onClick={() => onDenyPatternChange(editState.value)}
aria-label={t(
status === "denied"
? "chat:commandExecution.removeFromDenied"
: "chat:commandExecution.addToDenied",
)}>
<X className="size-3.5" />
</button>
</div>
</div>
)
})}
</div>
)}
</div>
)
}
Loading
Loading