import { useCallback, useState, memo, useMemo } from "react" import { useEvent } from "react-use" import { t } from "i18next" import { ChevronDown, OctagonX } from "lucide-react" import { type ExtensionMessage, type CommandExecutionStatus, commandExecutionStatusSchema } from "@roo-code/types" import { safeJsonParse } from "@roo/core" import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences" import { parseCommand } from "@roo/parse-command" import { vscode } from "@src/utils/vscode" import { extractPatternsFromCommand } from "@src/utils/command-parser" import { useExtensionState } from "@src/context/ExtensionStateContext" import { cn } from "@src/lib/utils" import { Button, StandardTooltip } from "@src/components/ui" import CodeBlock from "@src/components/common/CodeBlock" import { CommandPatternSelector } from "./CommandPatternSelector" interface CommandPattern { pattern: string description?: string } interface CommandExecutionProps { executionId: string text?: string icon?: JSX.Element | null title?: JSX.Element | null } export const CommandExecution = ({ executionId, text, icon, title }: CommandExecutionProps) => { const { terminalShellIntegrationDisabled = false, allowedCommands = [], deniedCommands = [], setAllowedCommands, setDeniedCommands, } = useExtensionState() const { command, output: parsedOutput } = useMemo(() => parseCommandAndOutput(text), [text]) // If we aren't opening the VSCode terminal for this command then we default // to expanding the command execution output. const [isExpanded, setIsExpanded] = useState(terminalShellIntegrationDisabled) const [streamingOutput, setStreamingOutput] = useState("") const [status, setStatus] = useState(null) // The command's output can either come from the text associated with the // task message (this is the case for completed commands) or from the // 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(() => { // First get all individual commands (including subshell commands) using parseCommand const allCommands = parseCommand(command) // Then extract patterns from each command using the existing pattern extraction logic const allPatterns = new Set() // Add all individual commands first allCommands.forEach((cmd) => { if (cmd.trim()) { allPatterns.add(cmd.trim()) } }) // Then add extracted patterns for each command allCommands.forEach((cmd) => { const patterns = extractPatternsFromCommand(cmd) patterns.forEach((pattern) => allPatterns.add(pattern)) }) return Array.from(allPatterns).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: "updateSettings", updatedSettings: { allowedCommands: newAllowed, deniedCommands: 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: "updateSettings", updatedSettings: { allowedCommands: newAllowed, deniedCommands: newDenied }, }) } const onMessage = useCallback( (event: MessageEvent) => { const message: ExtensionMessage = event.data if (message.type === "commandExecutionStatus") { const result = commandExecutionStatusSchema.safeParse(safeJsonParse(message.text, {})) if (result.success) { const data = result.data if (data.executionId !== executionId) { return } switch (data.status) { case "started": setStatus(data) break case "output": setStreamingOutput(data.output) break case "fallback": setIsExpanded(true) break default: setStatus(data) break } } } }, [executionId], ) useEvent("message", onMessage) return ( <>
{icon} {title} {status?.status === "exited" && (
)}
{status?.status === "started" && (
{status.pid &&
(PID: {status.pid})
}
)} {output.length > 0 && ( )}
{command && command.trim() && ( )}
) } CommandExecution.displayName = "CommandExecution" const OutputContainerInternal = ({ isExpanded, output }: { isExpanded: boolean; output: string }) => (
{output.length > 0 && }
) const OutputContainer = memo(OutputContainerInternal) const parseCommandAndOutput = (text: string | undefined) => { if (!text) { return { command: "", output: "" } } const index = text.indexOf(COMMAND_OUTPUT_STRING) if (index === -1) { return { command: text, output: "" } } return { command: text.slice(0, index), output: text.slice(index + COMMAND_OUTPUT_STRING.length), } }