mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: Add terminal command permissions UI to chat interface (#5480)
This commit is contained in:
parent
ba541ab848
commit
6fda13643c
24 changed files with 1512 additions and 5 deletions
|
|
@ -13,6 +13,13 @@ 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 {
|
||||
extractCommandPatterns,
|
||||
getPatternDescription,
|
||||
parseCommandAndOutput as parseCommandAndOutputUtil,
|
||||
CommandPattern,
|
||||
} from "../../utils/commandPatterns"
|
||||
|
||||
interface CommandExecutionProps {
|
||||
executionId: string
|
||||
|
|
@ -22,21 +29,91 @@ 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])
|
||||
const {
|
||||
command,
|
||||
output: parsedOutput,
|
||||
suggestions,
|
||||
} = useMemo(() => {
|
||||
// First try our enhanced parser
|
||||
const enhanced = parseCommandAndOutputUtil(text || "")
|
||||
// If it found a command, use it, otherwise fall back to the original parser
|
||||
if (enhanced.command && enhanced.command !== text) {
|
||||
return enhanced
|
||||
}
|
||||
// Fall back to original parser
|
||||
const original = parseCommandAndOutput(text)
|
||||
return { ...original, suggestions: [] }
|
||||
}, [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<CommandExecutionStatus | null>(null)
|
||||
const [showSuggestions] = useState(true)
|
||||
|
||||
// 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
|
||||
const commandPatterns = useMemo<CommandPattern[]>(() => {
|
||||
const patterns: CommandPattern[] = []
|
||||
|
||||
// Use AI suggestions if available
|
||||
if (suggestions.length > 0) {
|
||||
suggestions.forEach((suggestion) => {
|
||||
patterns.push({
|
||||
pattern: suggestion,
|
||||
description: getPatternDescription(suggestion),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Extract patterns programmatically
|
||||
const extractedPatterns = extractCommandPatterns(command)
|
||||
extractedPatterns.forEach((pattern) => {
|
||||
patterns.push({
|
||||
pattern,
|
||||
description: getPatternDescription(pattern),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return patterns
|
||||
}, [command, suggestions])
|
||||
|
||||
// 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
|
||||
|
|
@ -121,9 +198,20 @@ 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>
|
||||
{showSuggestions && commandPatterns.length > 0 && (
|
||||
<CommandPatternSelector
|
||||
patterns={commandPatterns}
|
||||
allowedCommands={allowedCommands}
|
||||
deniedCommands={deniedCommands}
|
||||
onAllowPatternChange={handleAllowPatternChange}
|
||||
onDenyPatternChange={handleDenyPatternChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
130
webview-ui/src/components/chat/CommandPatternSelector.tsx
Normal file
130
webview-ui/src/components/chat/CommandPatternSelector.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import React, { useState } 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 { CommandPattern } from "../../utils/commandPatterns"
|
||||
import { StandardTooltip } from "../ui/standard-tooltip"
|
||||
|
||||
interface CommandPatternSelectorProps {
|
||||
patterns: CommandPattern[]
|
||||
allowedCommands: string[]
|
||||
deniedCommands: string[]
|
||||
onAllowPatternChange: (pattern: string) => void
|
||||
onDenyPatternChange: (pattern: string) => void
|
||||
}
|
||||
|
||||
export const CommandPatternSelector: React.FC<CommandPatternSelectorProps> = ({
|
||||
patterns,
|
||||
allowedCommands,
|
||||
deniedCommands,
|
||||
onAllowPatternChange,
|
||||
onDenyPatternChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
const getPatternStatus = (pattern: string): "allowed" | "denied" | "none" => {
|
||||
if (allowedCommands.includes(pattern)) return "allowed"
|
||||
if (deniedCommands.includes(pattern)) return "denied"
|
||||
return "none"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-t border-vscode-panel-border bg-vscode-sideBar-background/30">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="flex items-center gap-2 w-full px-3 py-2 text-xs text-vscode-descriptionForeground hover:text-vscode-foreground hover:bg-vscode-list-hoverBackground transition-all"
|
||||
aria-expanded={isExpanded}
|
||||
aria-label={t(
|
||||
isExpanded ? "chat:commandExecution.collapseManagement" : "chat:commandExecution.expandManagement",
|
||||
)}>
|
||||
<ChevronDown
|
||||
className={cn("size-3 transition-transform duration-200", {
|
||||
"rotate-0": isExpanded,
|
||||
"-rotate-90": !isExpanded,
|
||||
})}
|
||||
/>
|
||||
<span className="font-medium">{t("chat:commandExecution.manageCommands")}</span>
|
||||
<StandardTooltip
|
||||
content={
|
||||
<Trans
|
||||
i18nKey="chat:commandExecution.commandManagementDescription"
|
||||
components={{
|
||||
settingsLink: (
|
||||
<VSCodeLink
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
window.postMessage(
|
||||
{
|
||||
type: "action",
|
||||
action: "settingsButtonClicked",
|
||||
values: { section: "autoApprove" },
|
||||
},
|
||||
"*",
|
||||
)
|
||||
}}
|
||||
className="inline"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}>
|
||||
<Info className="size-3 ml-1" />
|
||||
</StandardTooltip>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="px-3 pb-3 space-y-2">
|
||||
{patterns.map((item, index) => {
|
||||
const status = getPatternStatus(item.pattern)
|
||||
return (
|
||||
<div key={`${item.pattern}-${index}`} className="ml-5 flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<span className="font-mono text-xs text-vscode-foreground">{item.pattern}</span>
|
||||
{item.description && (
|
||||
<span className="text-xs text-vscode-descriptionForeground ml-2">
|
||||
- {item.description}
|
||||
</span>
|
||||
)}
|
||||
</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(item.pattern)}
|
||||
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(item.pattern)}
|
||||
aria-label={t(
|
||||
status === "denied"
|
||||
? "chat:commandExecution.removeFromDenied"
|
||||
: "chat:commandExecution.addToDenied",
|
||||
)}>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
import React from "react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { CommandExecution } from "../CommandExecution"
|
||||
import { ExtensionStateContext } from "../../../context/ExtensionStateContext"
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("react-use", () => ({
|
||||
useEvent: vi.fn(),
|
||||
}))
|
||||
|
||||
import { vscode } from "../../../utils/vscode"
|
||||
|
||||
vi.mock("../../../utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../../common/CodeBlock", () => ({
|
||||
default: ({ source }: { source: string }) => <div data-testid="code-block">{source}</div>,
|
||||
}))
|
||||
|
||||
vi.mock("../CommandPatternSelector", () => ({
|
||||
CommandPatternSelector: ({ patterns, onAllowPatternChange, onDenyPatternChange }: any) => (
|
||||
<div data-testid="command-pattern-selector">
|
||||
{patterns.map((p: any, i: number) => (
|
||||
<div key={i}>
|
||||
<span>{p.pattern}</span>
|
||||
<button onClick={() => onAllowPatternChange(p.pattern)}>Allow {p.pattern}</button>
|
||||
<button onClick={() => onDenyPatternChange(p.pattern)}>Deny {p.pattern}</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock ExtensionStateContext
|
||||
const mockExtensionState = {
|
||||
terminalShellIntegrationDisabled: false,
|
||||
allowedCommands: ["npm"],
|
||||
deniedCommands: ["rm"],
|
||||
setAllowedCommands: vi.fn(),
|
||||
setDeniedCommands: vi.fn(),
|
||||
}
|
||||
|
||||
const ExtensionStateWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<ExtensionStateContext.Provider value={mockExtensionState as any}>{children}</ExtensionStateContext.Provider>
|
||||
)
|
||||
|
||||
describe("CommandExecution", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should render command without output", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="npm install" />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("code-block")).toHaveTextContent("npm install")
|
||||
})
|
||||
|
||||
it("should render command with output", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="npm install\n\nCommand Output:\nInstalling packages..." />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
const codeBlocks = screen.getAllByTestId("code-block")
|
||||
expect(codeBlocks[0]).toHaveTextContent("npm install")
|
||||
})
|
||||
|
||||
it("should render with custom icon and title", () => {
|
||||
const icon = <span data-testid="custom-icon">📦</span>
|
||||
const title = <span data-testid="custom-title">Installing Dependencies</span>
|
||||
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="npm install" icon={icon} title={title} />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("custom-icon")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("custom-title")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should show command pattern selector for simple commands", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="npm install express" />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("command-pattern-selector")).toBeInTheDocument()
|
||||
expect(screen.getByText("npm")).toBeInTheDocument()
|
||||
expect(screen.getByText("npm install")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should handle allow pattern change", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="git push" />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
const allowButton = screen.getByText("Allow git")
|
||||
fireEvent.click(allowButton)
|
||||
|
||||
expect(mockExtensionState.setAllowedCommands).toHaveBeenCalledWith(["npm", "git"])
|
||||
expect(mockExtensionState.setDeniedCommands).toHaveBeenCalledWith(["rm"])
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "allowedCommands", commands: ["npm", "git"] })
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "deniedCommands", commands: ["rm"] })
|
||||
})
|
||||
|
||||
it("should handle deny pattern change", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="docker run" />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
const denyButton = screen.getByText("Deny docker")
|
||||
fireEvent.click(denyButton)
|
||||
|
||||
expect(mockExtensionState.setAllowedCommands).toHaveBeenCalledWith(["npm"])
|
||||
expect(mockExtensionState.setDeniedCommands).toHaveBeenCalledWith(["rm", "docker"])
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "allowedCommands", commands: ["npm"] })
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "deniedCommands", commands: ["rm", "docker"] })
|
||||
})
|
||||
|
||||
it("should toggle allowed pattern", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="npm test" />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
const allowButton = screen.getByText("Allow npm")
|
||||
fireEvent.click(allowButton)
|
||||
|
||||
// npm is already in allowedCommands, so it should be removed
|
||||
expect(mockExtensionState.setAllowedCommands).toHaveBeenCalledWith([])
|
||||
expect(mockExtensionState.setDeniedCommands).toHaveBeenCalledWith(["rm"])
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "allowedCommands", commands: [] })
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "deniedCommands", commands: ["rm"] })
|
||||
})
|
||||
|
||||
it("should toggle denied pattern", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="rm -rf" />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
const denyButton = screen.getByText("Deny rm")
|
||||
fireEvent.click(denyButton)
|
||||
|
||||
// rm is already in deniedCommands, so it should be removed
|
||||
expect(mockExtensionState.setAllowedCommands).toHaveBeenCalledWith(["npm"])
|
||||
expect(mockExtensionState.setDeniedCommands).toHaveBeenCalledWith([])
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "allowedCommands", commands: ["npm"] })
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "deniedCommands", commands: [] })
|
||||
})
|
||||
|
||||
it("should parse command with $ prefix", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="$ npm install\nInstalling..." />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("code-block")).toHaveTextContent("npm install")
|
||||
})
|
||||
|
||||
it("should parse command with AI suggestions", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution
|
||||
executionId="test-1"
|
||||
text="$ npm install\nSuggested patterns: npm, npm install, npm run"
|
||||
/>
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("command-pattern-selector")).toBeInTheDocument()
|
||||
// Check that the patterns are present in the mock
|
||||
expect(screen.getByText("npm")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should handle commands with pipes", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="ls -la | grep test" />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("command-pattern-selector")).toBeInTheDocument()
|
||||
expect(screen.getByText("ls")).toBeInTheDocument()
|
||||
expect(screen.getByText("grep")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should handle commands with && operator", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="npm install && npm test" />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("command-pattern-selector")).toBeInTheDocument()
|
||||
expect(screen.getByText("npm")).toBeInTheDocument()
|
||||
expect(screen.getByText("npm install")).toBeInTheDocument()
|
||||
expect(screen.getByText("npm test")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should not show pattern selector for empty commands", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="" />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId("command-pattern-selector")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should expand output when terminal shell integration is disabled", () => {
|
||||
const disabledState = {
|
||||
...mockExtensionState,
|
||||
terminalShellIntegrationDisabled: true,
|
||||
}
|
||||
|
||||
render(
|
||||
<ExtensionStateContext.Provider value={disabledState as any}>
|
||||
<CommandExecution executionId="test-1" text="npm install\n\nCommand Output:\nOutput here" />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
// Output should be visible when shell integration is disabled
|
||||
expect(screen.getByText(/Output here/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should handle undefined allowedCommands and deniedCommands", () => {
|
||||
const stateWithUndefined = {
|
||||
...mockExtensionState,
|
||||
allowedCommands: undefined,
|
||||
deniedCommands: undefined,
|
||||
}
|
||||
|
||||
render(
|
||||
<ExtensionStateContext.Provider value={stateWithUndefined as any}>
|
||||
<CommandExecution executionId="test-1" text="npm install" />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("command-pattern-selector")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should handle pattern change when moving from denied to allowed", () => {
|
||||
render(
|
||||
<ExtensionStateWrapper>
|
||||
<CommandExecution executionId="test-1" text="rm file.txt" />
|
||||
</ExtensionStateWrapper>,
|
||||
)
|
||||
|
||||
const allowButton = screen.getByText("Allow rm")
|
||||
fireEvent.click(allowButton)
|
||||
|
||||
// rm should be removed from denied and added to allowed
|
||||
expect(mockExtensionState.setAllowedCommands).toHaveBeenCalledWith(["npm", "rm"])
|
||||
expect(mockExtensionState.setDeniedCommands).toHaveBeenCalledWith([])
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "allowedCommands", commands: ["npm", "rm"] })
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "deniedCommands", commands: [] })
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
import React from "react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { CommandPatternSelector } from "../CommandPatternSelector"
|
||||
import { CommandPattern } from "../../../utils/commandPatterns"
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
Trans: ({ i18nKey, components }: any) => {
|
||||
if (i18nKey === "chat:commandExecution.commandManagementDescription") {
|
||||
return (
|
||||
<span>
|
||||
Manage command permissions: Click ✓ to allow auto-execution, ✗ to deny execution. Patterns can be
|
||||
toggled on/off or removed from lists. {components.settingsLink}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <span>{i18nKey}</span>
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock VSCodeLink
|
||||
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
|
||||
VSCodeLink: ({ children, onClick }: any) => (
|
||||
<a href="#" onClick={onClick}>
|
||||
{children || "View all settings"}
|
||||
</a>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock StandardTooltip
|
||||
vi.mock("../../ui/standard-tooltip", () => ({
|
||||
StandardTooltip: ({ children, content }: any) => (
|
||||
<div title={typeof content === "string" ? content : "tooltip"}>
|
||||
{children}
|
||||
{/* Render the content to make it testable */}
|
||||
<div style={{ display: "none" }}>{content}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock window.postMessage
|
||||
const mockPostMessage = vi.fn()
|
||||
window.postMessage = mockPostMessage
|
||||
|
||||
describe("CommandPatternSelector", () => {
|
||||
const mockPatterns: CommandPattern[] = [
|
||||
{ pattern: "npm", description: "npm commands" },
|
||||
{ pattern: "npm install", description: "npm install commands" },
|
||||
{ pattern: "git", description: "git commands" },
|
||||
]
|
||||
|
||||
const defaultProps = {
|
||||
patterns: mockPatterns,
|
||||
allowedCommands: ["npm"],
|
||||
deniedCommands: ["git"],
|
||||
onAllowPatternChange: vi.fn(),
|
||||
onDenyPatternChange: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should render collapsed by default", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText("chat:commandExecution.manageCommands")).toBeInTheDocument()
|
||||
expect(screen.queryByText("npm commands")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should expand when clicked", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Check for the patterns themselves
|
||||
expect(screen.getByText("npm")).toBeInTheDocument()
|
||||
expect(screen.getByText("npm install")).toBeInTheDocument()
|
||||
expect(screen.getByText("git")).toBeInTheDocument()
|
||||
|
||||
// Check for the descriptions
|
||||
expect(screen.getByText("- npm commands")).toBeInTheDocument()
|
||||
expect(screen.getByText("- npm install commands")).toBeInTheDocument()
|
||||
expect(screen.getByText("- git commands")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should collapse when clicked again", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
const collapseButton = screen.getByRole("button", { name: "chat:commandExecution.collapseManagement" })
|
||||
fireEvent.click(collapseButton)
|
||||
|
||||
expect(screen.queryByText("npm commands")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should show correct status for patterns", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Check that npm has allowed styling (green)
|
||||
const npmAllowButton = screen.getAllByRole("button", { name: "chat:commandExecution.removeFromAllowed" })[0]
|
||||
expect(npmAllowButton).toHaveClass("bg-green-500/20")
|
||||
|
||||
// Check that git has denied styling (red)
|
||||
const gitDenyButton = screen.getAllByRole("button", { name: "chat:commandExecution.removeFromDenied" })[0]
|
||||
expect(gitDenyButton).toHaveClass("bg-red-500/20")
|
||||
})
|
||||
|
||||
it("should call onAllowPatternChange when allow button is clicked", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Find all allow buttons with the "add to allowed" label
|
||||
const allowButtons = screen.getAllByRole("button", { name: "chat:commandExecution.addToAllowed" })
|
||||
|
||||
// The second one should be for npm install (first is npm which is already allowed)
|
||||
fireEvent.click(allowButtons[0])
|
||||
|
||||
expect(defaultProps.onAllowPatternChange).toHaveBeenCalledWith("npm install")
|
||||
})
|
||||
|
||||
it("should call onDenyPatternChange when deny button is clicked", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Find all deny buttons with the "add to denied" label
|
||||
const denyButtons = screen.getAllByRole("button", { name: "chat:commandExecution.addToDenied" })
|
||||
|
||||
// The second one should be for npm install (first is npm, third is git which is already denied)
|
||||
fireEvent.click(denyButtons[1])
|
||||
|
||||
expect(defaultProps.onDenyPatternChange).toHaveBeenCalledWith("npm install")
|
||||
})
|
||||
|
||||
it("should toggle allowed pattern when clicked", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Find the allow button for npm (which is already allowed)
|
||||
const npmAllowButton = screen.getAllByRole("button", { name: "chat:commandExecution.removeFromAllowed" })[0]
|
||||
fireEvent.click(npmAllowButton)
|
||||
|
||||
expect(defaultProps.onAllowPatternChange).toHaveBeenCalledWith("npm")
|
||||
})
|
||||
|
||||
it("should toggle denied pattern when clicked", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// Find the deny button for git (which is already denied)
|
||||
const gitDenyButton = screen.getAllByRole("button", { name: "chat:commandExecution.removeFromDenied" })[0]
|
||||
fireEvent.click(gitDenyButton)
|
||||
|
||||
expect(defaultProps.onDenyPatternChange).toHaveBeenCalledWith("git")
|
||||
})
|
||||
|
||||
it("should have tooltip with settings link", () => {
|
||||
const { container } = render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
// The info icon should have a tooltip
|
||||
const tooltipWrapper = container.querySelector('[title="tooltip"]')
|
||||
expect(tooltipWrapper).toBeTruthy()
|
||||
|
||||
// The tooltip content includes a settings link (mocked as VSCodeLink)
|
||||
// It's rendered in a hidden div for testing purposes
|
||||
const settingsLink = container.querySelector('a[href="#"]')
|
||||
expect(settingsLink).toBeTruthy()
|
||||
expect(settingsLink?.textContent).toBe("View all settings")
|
||||
|
||||
// Test that clicking the link posts the correct message
|
||||
if (settingsLink) {
|
||||
fireEvent.click(settingsLink)
|
||||
|
||||
expect(mockPostMessage).toHaveBeenCalledWith(
|
||||
{
|
||||
type: "action",
|
||||
action: "settingsButtonClicked",
|
||||
values: { section: "autoApprove" },
|
||||
},
|
||||
"*",
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("should render with empty patterns", () => {
|
||||
render(<CommandPatternSelector {...defaultProps} patterns={[]} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// The expanded view should exist but be empty since there are no patterns
|
||||
const expandedContent = screen
|
||||
.getByRole("button", { name: "chat:commandExecution.collapseManagement" })
|
||||
.parentElement?.querySelector(".px-3.pb-3")
|
||||
expect(expandedContent).toBeInTheDocument()
|
||||
expect(expandedContent?.children.length).toBe(0)
|
||||
})
|
||||
|
||||
it("should render patterns without descriptions", () => {
|
||||
const patternsWithoutDesc: CommandPattern[] = [{ pattern: "custom-command" }]
|
||||
|
||||
render(<CommandPatternSelector {...defaultProps} patterns={patternsWithoutDesc} />)
|
||||
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
expect(screen.getByText("custom-command")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should always show info icon with tooltip", () => {
|
||||
const { container } = render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
// Info icon should always be visible (not just when expanded)
|
||||
// Look for the Info icon which is wrapped in StandardTooltip
|
||||
const infoIcon = container.querySelector(".ml-1")
|
||||
expect(infoIcon).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should apply correct classes for chevron rotation", () => {
|
||||
const { container } = render(<CommandPatternSelector {...defaultProps} />)
|
||||
|
||||
// Initially collapsed - chevron should be rotated
|
||||
let chevron = container.querySelector(".size-3.transition-transform")
|
||||
expect(chevron).toHaveClass("-rotate-90")
|
||||
|
||||
// Click to expand
|
||||
const expandButton = screen.getByRole("button", { name: "chat:commandExecution.expandManagement" })
|
||||
fireEvent.click(expandButton)
|
||||
|
||||
// When expanded - chevron should not be rotated
|
||||
chevron = container.querySelector(".size-3.transition-transform")
|
||||
expect(chevron).toHaveClass("rotate-0")
|
||||
})
|
||||
})
|
||||
16
webview-ui/src/i18n/locales/ca/chat.json
generated
16
webview-ui/src/i18n/locales/ca/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo ha vist noms de definicions de codi font utilitzats en aquest directori (fora de l'espai de treball):"
|
||||
},
|
||||
"commandOutput": "Sortida de l'ordre",
|
||||
"commandExecution": {
|
||||
"running": "Executant",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Finalitzat ({{exitCode}})",
|
||||
"manageCommands": "Gestiona els permisos de les ordres",
|
||||
"commandManagementDescription": "Gestiona els permisos de les ordres: Fes clic a ✓ per permetre l'execució automàtica, ✗ per denegar l'execució. Els patrons es poden activar/desactivar o eliminar de les llistes. <settingsLink>Mostra tots els paràmetres</settingsLink>",
|
||||
"addToAllowed": "Afegeix a la llista de permesos",
|
||||
"removeFromAllowed": "Elimina de la llista de permesos",
|
||||
"addToDenied": "Afegeix a la llista de denegats",
|
||||
"removeFromDenied": "Elimina de la llista de denegats",
|
||||
"abortCommand": "Interromp l'execució de l'ordre",
|
||||
"expandOutput": "Amplia la sortida",
|
||||
"collapseOutput": "Redueix la sortida",
|
||||
"expandManagement": "Amplia la secció de gestió d'ordres",
|
||||
"collapseManagement": "Redueix la secció de gestió d'ordres"
|
||||
},
|
||||
"response": "Resposta",
|
||||
"arguments": "Arguments",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/de/chat.json
generated
16
webview-ui/src/i18n/locales/de/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo hat Quellcode-Definitionsnamen in diesem Verzeichnis (außerhalb des Arbeitsbereichs) angezeigt:"
|
||||
},
|
||||
"commandOutput": "Befehlsausgabe",
|
||||
"commandExecution": {
|
||||
"running": "Wird ausgeführt",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Beendet ({{exitCode}})",
|
||||
"manageCommands": "Befehlsberechtigungen verwalten",
|
||||
"commandManagementDescription": "Befehlsberechtigungen verwalten: Klicke auf ✓, um die automatische Ausführung zu erlauben, ✗, um die Ausführung zu verweigern. Muster können ein-/ausgeschaltet oder aus Listen entfernt werden. <settingsLink>Alle Einstellungen anzeigen</settingsLink>",
|
||||
"addToAllowed": "Zur Liste der erlaubten Befehle hinzufügen",
|
||||
"removeFromAllowed": "Von der Liste der erlaubten Befehle entfernen",
|
||||
"addToDenied": "Zur Liste der verweigerten Befehle hinzufügen",
|
||||
"removeFromDenied": "Von der Liste der verweigerten Befehle entfernen",
|
||||
"abortCommand": "Befehlsausführung abbrechen",
|
||||
"expandOutput": "Ausgabe erweitern",
|
||||
"collapseOutput": "Ausgabe einklappen",
|
||||
"expandManagement": "Befehlsverwaltungsbereich erweitern",
|
||||
"collapseManagement": "Befehlsverwaltungsbereich einklappen"
|
||||
},
|
||||
"response": "Antwort",
|
||||
"arguments": "Argumente",
|
||||
"mcp": {
|
||||
|
|
|
|||
|
|
@ -211,6 +211,22 @@
|
|||
"resultTooltip": "Similarity score: {{score}} (click to open file)"
|
||||
},
|
||||
"commandOutput": "Command Output",
|
||||
"commandExecution": {
|
||||
"running": "Running",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Exited ({{exitCode}})",
|
||||
"manageCommands": "Manage Command Permissions",
|
||||
"commandManagementDescription": "Manage command permissions: Click ✓ to allow auto-execution, ✗ to deny execution. Patterns can be toggled on/off or removed from lists. <settingsLink>View all settings</settingsLink>",
|
||||
"addToAllowed": "Add to allowed list",
|
||||
"removeFromAllowed": "Remove from allowed list",
|
||||
"addToDenied": "Add to denied list",
|
||||
"removeFromDenied": "Remove from denied list",
|
||||
"abortCommand": "Abort command execution",
|
||||
"expandOutput": "Expand output",
|
||||
"collapseOutput": "Collapse output",
|
||||
"expandManagement": "Expand command management section",
|
||||
"collapseManagement": "Collapse command management section"
|
||||
},
|
||||
"response": "Response",
|
||||
"arguments": "Arguments",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/es/chat.json
generated
16
webview-ui/src/i18n/locales/es/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo vio nombres de definiciones de código fuente utilizados en este directorio (fuera del espacio de trabajo):"
|
||||
},
|
||||
"commandOutput": "Salida del comando",
|
||||
"commandExecution": {
|
||||
"running": "Ejecutando",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Finalizado ({{exitCode}})",
|
||||
"manageCommands": "Gestionar permisos de comandos",
|
||||
"commandManagementDescription": "Gestionar permisos de comandos: Haz clic en ✓ para permitir la ejecución automática, ✗ para denegar la ejecución. Los patrones se pueden activar/desactivar o eliminar de las listas. <settingsLink>Ver todos los ajustes</settingsLink>",
|
||||
"addToAllowed": "Añadir a la lista de permitidos",
|
||||
"removeFromAllowed": "Eliminar de la lista de permitidos",
|
||||
"addToDenied": "Añadir a la lista de denegados",
|
||||
"removeFromDenied": "Eliminar de la lista de denegados",
|
||||
"abortCommand": "Abortar ejecución del comando",
|
||||
"expandOutput": "Expandir salida",
|
||||
"collapseOutput": "Contraer salida",
|
||||
"expandManagement": "Expandir sección de gestión de comandos",
|
||||
"collapseManagement": "Contraer sección de gestión de comandos"
|
||||
},
|
||||
"response": "Respuesta",
|
||||
"arguments": "Argumentos",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/fr/chat.json
generated
16
webview-ui/src/i18n/locales/fr/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo a vu les noms de définitions de code source utilisés dans ce répertoire (hors espace de travail) :"
|
||||
},
|
||||
"commandOutput": "Sortie de commande",
|
||||
"commandExecution": {
|
||||
"running": "En cours d'exécution",
|
||||
"pid": "PID : {{pid}}",
|
||||
"exited": "Terminé ({{exitCode}})",
|
||||
"manageCommands": "Gérer les autorisations de commande",
|
||||
"commandManagementDescription": "Gérer les autorisations de commande : Cliquez sur ✓ pour autoriser l'exécution automatique, ✗ pour refuser l'exécution. Les modèles peuvent être activés/désactivés ou supprimés des listes. <settingsLink>Voir tous les paramètres</settingsLink>",
|
||||
"addToAllowed": "Ajouter à la liste autorisée",
|
||||
"removeFromAllowed": "Retirer de la liste autorisée",
|
||||
"addToDenied": "Ajouter à la liste refusée",
|
||||
"removeFromDenied": "Retirer de la liste refusée",
|
||||
"abortCommand": "Abandonner l'exécution de la commande",
|
||||
"expandOutput": "Développer la sortie",
|
||||
"collapseOutput": "Réduire la sortie",
|
||||
"expandManagement": "Développer la section de gestion des commandes",
|
||||
"collapseManagement": "Réduire la section de gestion des commandes"
|
||||
},
|
||||
"response": "Réponse",
|
||||
"arguments": "Arguments",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/hi/chat.json
generated
16
webview-ui/src/i18n/locales/hi/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में उपयोग किए गए सोर्स कोड परिभाषा नामों को देखा:"
|
||||
},
|
||||
"commandOutput": "कमांड आउटपुट",
|
||||
"commandExecution": {
|
||||
"running": "चलाया जा रहा है",
|
||||
"pid": "पीआईडी: {{pid}}",
|
||||
"exited": "बाहर निकल गया ({{exitCode}})",
|
||||
"manageCommands": "कमांड अनुमतियाँ प्रबंधित करें",
|
||||
"commandManagementDescription": "कमांड अनुमतियों का प्रबंधन करें: स्वतः-निष्पादन की अनुमति देने के लिए ✓ पर क्लिक करें, निष्पादन से इनकार करने के लिए ✗ पर क्लिक करें। पैटर्न को चालू/बंद किया जा सकता है या सूचियों से हटाया जा सकता है। <settingsLink>सभी सेटिंग्स देखें</settingsLink>",
|
||||
"addToAllowed": "अनुमत सूची में जोड़ें",
|
||||
"removeFromAllowed": "अनुमत सूची से हटाएं",
|
||||
"addToDenied": "अस्वीकृत सूची में जोड़ें",
|
||||
"removeFromDenied": "अस्वीकृत सूची से हटाएं",
|
||||
"abortCommand": "कमांड निष्पादन रद्द करें",
|
||||
"expandOutput": "आउटपुट का विस्तार करें",
|
||||
"collapseOutput": "आउटपुट संक्षिप्त करें",
|
||||
"expandManagement": "कमांड प्रबंधन अनुभाग का विस्तार करें",
|
||||
"collapseManagement": "कमांड प्रबंधन अनुभाग संक्षिप्त करें"
|
||||
},
|
||||
"response": "प्रतिक्रिया",
|
||||
"arguments": "आर्ग्युमेंट्स",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/id/chat.json
generated
16
webview-ui/src/i18n/locales/id/chat.json
generated
|
|
@ -214,6 +214,22 @@
|
|||
"resultTooltip": "Skor kemiripan: {{score}} (klik untuk membuka file)"
|
||||
},
|
||||
"commandOutput": "Output Perintah",
|
||||
"commandExecution": {
|
||||
"running": "Menjalankan",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Keluar ({{exitCode}})",
|
||||
"manageCommands": "Kelola Izin Perintah",
|
||||
"commandManagementDescription": "Kelola izin perintah: Klik ✓ untuk mengizinkan eksekusi otomatis, ✗ untuk menolak eksekusi. Pola dapat diaktifkan/dinonaktifkan atau dihapus dari daftar. <settingsLink>Lihat semua pengaturan</settingsLink>",
|
||||
"addToAllowed": "Tambahkan ke daftar yang diizinkan",
|
||||
"removeFromAllowed": "Hapus dari daftar yang diizinkan",
|
||||
"addToDenied": "Tambahkan ke daftar yang ditolak",
|
||||
"removeFromDenied": "Hapus dari daftar yang ditolak",
|
||||
"abortCommand": "Batalkan eksekusi perintah",
|
||||
"expandOutput": "Perluas output",
|
||||
"collapseOutput": "Ciutkan output",
|
||||
"expandManagement": "Perluas bagian manajemen perintah",
|
||||
"collapseManagement": "Ciutkan bagian manajemen perintah"
|
||||
},
|
||||
"response": "Respons",
|
||||
"arguments": "Argumen",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/it/chat.json
generated
16
webview-ui/src/i18n/locales/it/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo ha visualizzato i nomi delle definizioni di codice sorgente utilizzate in questa directory (fuori dall'area di lavoro):"
|
||||
},
|
||||
"commandOutput": "Output del comando",
|
||||
"commandExecution": {
|
||||
"running": "In esecuzione",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Terminato ({{exitCode}})",
|
||||
"manageCommands": "Gestisci autorizzazioni comandi",
|
||||
"commandManagementDescription": "Gestisci le autorizzazioni dei comandi: fai clic su ✓ per consentire l'esecuzione automatica, ✗ per negare l'esecuzione. I pattern possono essere attivati/disattivati o rimossi dagli elenchi. <settingsLink>Visualizza tutte le impostazioni</settingsLink>",
|
||||
"addToAllowed": "Aggiungi all'elenco consentiti",
|
||||
"removeFromAllowed": "Rimuovi dall'elenco consentiti",
|
||||
"addToDenied": "Aggiungi all'elenco negati",
|
||||
"removeFromDenied": "Rimuovi dall'elenco negati",
|
||||
"abortCommand": "Interrompi esecuzione comando",
|
||||
"expandOutput": "Espandi output",
|
||||
"collapseOutput": "Comprimi output",
|
||||
"expandManagement": "Espandi la sezione di gestione dei comandi",
|
||||
"collapseManagement": "Comprimi la sezione di gestione dei comandi"
|
||||
},
|
||||
"response": "Risposta",
|
||||
"arguments": "Argomenti",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/ja/chat.json
generated
16
webview-ui/src/i18n/locales/ja/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)で使用されているソースコード定義名を表示しました:"
|
||||
},
|
||||
"commandOutput": "コマンド出力",
|
||||
"commandExecution": {
|
||||
"running": "実行中",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "終了しました ({{exitCode}})",
|
||||
"manageCommands": "コマンド権限の管理",
|
||||
"commandManagementDescription": "コマンドの権限を管理します:✓ をクリックして自動実行を許可し、✗ をクリックして実行を拒否します。パターンはオン/オフの切り替えやリストからの削除が可能です。<settingsLink>すべての設定を表示</settingsLink>",
|
||||
"addToAllowed": "許可リストに追加",
|
||||
"removeFromAllowed": "許可リストから削除",
|
||||
"addToDenied": "拒否リストに追加",
|
||||
"removeFromDenied": "拒否リストから削除",
|
||||
"abortCommand": "コマンドの実行を中止",
|
||||
"expandOutput": "出力を展開",
|
||||
"collapseOutput": "出力を折りたたむ",
|
||||
"expandManagement": "コマンド管理セクションを展開",
|
||||
"collapseManagement": "コマンド管理セクションを折りたたむ"
|
||||
},
|
||||
"response": "応答",
|
||||
"arguments": "引数",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/ko/chat.json
generated
16
webview-ui/src/i18n/locales/ko/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)에서 사용된 소스 코드 정의 이름을 보았습니다:"
|
||||
},
|
||||
"commandOutput": "명령 출력",
|
||||
"commandExecution": {
|
||||
"running": "실행 중",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "종료됨 ({{exitCode}})",
|
||||
"manageCommands": "명령 권한 관리",
|
||||
"commandManagementDescription": "명령 권한 관리: 자동 실행을 허용하려면 ✓를 클릭하고 실행을 거부하려면 ✗를 클릭하십시오. 패턴은 켜거나 끄거나 목록에서 제거할 수 있습니다. <settingsLink>모든 설정 보기</settingsLink>",
|
||||
"addToAllowed": "허용 목록에 추가",
|
||||
"removeFromAllowed": "허용 목록에서 제거",
|
||||
"addToDenied": "거부 목록에 추가",
|
||||
"removeFromDenied": "거부 목록에서 제거",
|
||||
"abortCommand": "명령 실행 중단",
|
||||
"expandOutput": "출력 확장",
|
||||
"collapseOutput": "출력 축소",
|
||||
"expandManagement": "명령 관리 섹션 확장",
|
||||
"collapseManagement": "명령 관리 섹션 축소"
|
||||
},
|
||||
"response": "응답",
|
||||
"arguments": "인수",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/nl/chat.json
generated
16
webview-ui/src/i18n/locales/nl/chat.json
generated
|
|
@ -187,6 +187,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo heeft broncode-definitienamen bekeken die in deze map (buiten werkruimte) worden gebruikt:"
|
||||
},
|
||||
"commandOutput": "Commando-uitvoer",
|
||||
"commandExecution": {
|
||||
"running": "Lopend",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Afgesloten ({{exitCode}})",
|
||||
"manageCommands": "Beheer Commando Toestemmingen",
|
||||
"commandManagementDescription": "Beheer commando toestemmingen: Klik op ✓ om automatische uitvoering toe te staan, ✗ om uitvoering te weigeren. Patronen kunnen worden in- of uitgeschakeld of uit lijsten worden verwijderd. <settingsLink>Bekijk alle instellingen</settingsLink>",
|
||||
"addToAllowed": "Toevoegen aan toegestane lijst",
|
||||
"removeFromAllowed": "Verwijderen van toegestane lijst",
|
||||
"addToDenied": "Toevoegen aan geweigerde lijst",
|
||||
"removeFromDenied": "Verwijderen van geweigerde lijst",
|
||||
"abortCommand": "Commando-uitvoering afbreken",
|
||||
"expandOutput": "Uitvoer uitvouwen",
|
||||
"collapseOutput": "Uitvoer samenvouwen",
|
||||
"expandManagement": "Beheersectie voor commando's uitvouwen",
|
||||
"collapseManagement": "Beheersectie voor commando's samenvouwen"
|
||||
},
|
||||
"response": "Antwoord",
|
||||
"arguments": "Argumenten",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/pl/chat.json
generated
16
webview-ui/src/i18n/locales/pl/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo zobaczył nazwy definicji kodu źródłowego używane w tym katalogu (poza obszarem roboczym):"
|
||||
},
|
||||
"commandOutput": "Wyjście polecenia",
|
||||
"commandExecution": {
|
||||
"running": "Wykonywanie",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Zakończono ({{exitCode}})",
|
||||
"manageCommands": "Zarządzaj uprawnieniami poleceń",
|
||||
"commandManagementDescription": "Zarządzaj uprawnieniami poleceń: Kliknij ✓, aby zezwolić na automatyczne wykonanie, ✗, aby odmówić wykonania. Wzorce można włączać/wyłączać lub usuwać z list. <settingsLink>Zobacz wszystkie ustawienia</settingsLink>",
|
||||
"addToAllowed": "Dodaj do listy dozwolonych",
|
||||
"removeFromAllowed": "Usuń z listy dozwolonych",
|
||||
"addToDenied": "Dodaj do listy odrzuconych",
|
||||
"removeFromDenied": "Usuń z listy odrzuconych",
|
||||
"abortCommand": "Przerwij wykonywanie polecenia",
|
||||
"expandOutput": "Rozwiń wyjście",
|
||||
"collapseOutput": "Zwiń wyjście",
|
||||
"expandManagement": "Rozwiń sekcję zarządzania poleceniami",
|
||||
"collapseManagement": "Zwiń sekcję zarządzania poleceniami"
|
||||
},
|
||||
"response": "Odpowiedź",
|
||||
"arguments": "Argumenty",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/pt-BR/chat.json
generated
16
webview-ui/src/i18n/locales/pt-BR/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo visualizou nomes de definição de código-fonte usados neste diretório (fora do espaço de trabalho):"
|
||||
},
|
||||
"commandOutput": "Saída do comando",
|
||||
"commandExecution": {
|
||||
"running": "Executando",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Encerrado ({{exitCode}})",
|
||||
"manageCommands": "Gerenciar Permissões de Comando",
|
||||
"commandManagementDescription": "Gerencie as permissões de comando: Clique em ✓ para permitir a execução automática, ✗ para negar a execução. Os padrões podem ser ativados/desativados ou removidos das listas. <settingsLink>Ver todas as configurações</settingsLink>",
|
||||
"addToAllowed": "Adicionar à lista de permitidos",
|
||||
"removeFromAllowed": "Remover da lista de permitidos",
|
||||
"addToDenied": "Adicionar à lista de negados",
|
||||
"removeFromDenied": "Remover da lista de negados",
|
||||
"abortCommand": "Abortar execução do comando",
|
||||
"expandOutput": "Expandir saída",
|
||||
"collapseOutput": "Recolher saída",
|
||||
"expandManagement": "Expandir seção de gerenciamento de comandos",
|
||||
"collapseManagement": "Recolher seção de gerenciamento de comandos"
|
||||
},
|
||||
"response": "Resposta",
|
||||
"arguments": "Argumentos",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/ru/chat.json
generated
16
webview-ui/src/i18n/locales/ru/chat.json
generated
|
|
@ -187,6 +187,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo просмотрел имена определений исходного кода в этой директории (вне рабочего пространства):"
|
||||
},
|
||||
"commandOutput": "Вывод команды",
|
||||
"commandExecution": {
|
||||
"running": "Выполняется",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Завершено ({{exitCode}})",
|
||||
"manageCommands": "Управление разрешениями команд",
|
||||
"commandManagementDescription": "Управляйте разрешениями команд: Нажмите ✓, чтобы разрешить автоматическое выполнение, ✗, чтобы запретить выполнение. Шаблоны можно включать/выключать или удалять из списков. <settingsLink>Просмотреть все настройки</settingsLink>",
|
||||
"addToAllowed": "Добавить в список разрешенных",
|
||||
"removeFromAllowed": "Удалить из списка разрешенных",
|
||||
"addToDenied": "Добавить в список запрещенных",
|
||||
"removeFromDenied": "Удалить из списка запрещенных",
|
||||
"abortCommand": "Прервать выполнение команды",
|
||||
"expandOutput": "Развернуть вывод",
|
||||
"collapseOutput": "Свернуть вывод",
|
||||
"expandManagement": "Развернуть раздел управления командами",
|
||||
"collapseManagement": "Свернуть раздел управления командами"
|
||||
},
|
||||
"response": "Ответ",
|
||||
"arguments": "Аргументы",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/tr/chat.json
generated
16
webview-ui/src/i18n/locales/tr/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo bu dizinde (çalışma alanı dışında) kullanılan kaynak kod tanımlama isimlerini görüntüledi:"
|
||||
},
|
||||
"commandOutput": "Komut Çıktısı",
|
||||
"commandExecution": {
|
||||
"running": "Çalışıyor",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Çıkıldı ({{exitCode}})",
|
||||
"manageCommands": "Komut İzinlerini Yönet",
|
||||
"commandManagementDescription": "Komut izinlerini yönetin: Otomatik yürütmeye izin vermek için ✓'e, yürütmeyi reddetmek için ✗'e tıklayın. Desenler açılıp kapatılabilir veya listelerden kaldırılabilir. <settingsLink>Tüm ayarları görüntüle</settingsLink>",
|
||||
"addToAllowed": "İzin verilenler listesine ekle",
|
||||
"removeFromAllowed": "İzin verilenler listesinden kaldır",
|
||||
"addToDenied": "Reddedilenler listesine ekle",
|
||||
"removeFromDenied": "Reddedilenler listesinden kaldır",
|
||||
"abortCommand": "Komut yürütmeyi iptal et",
|
||||
"expandOutput": "Çıktıyı genişlet",
|
||||
"collapseOutput": "Çıktıyı daralt",
|
||||
"expandManagement": "Komut yönetimi bölümünü genişlet",
|
||||
"collapseManagement": "Komut yönetimi bölümünü daralt"
|
||||
},
|
||||
"response": "Yanıt",
|
||||
"arguments": "Argümanlar",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/vi/chat.json
generated
16
webview-ui/src/i18n/locales/vi/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo đã xem tên định nghĩa mã nguồn được sử dụng trong thư mục này (ngoài không gian làm việc):"
|
||||
},
|
||||
"commandOutput": "Kết quả lệnh",
|
||||
"commandExecution": {
|
||||
"running": "Đang chạy",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "Đã thoát ({{exitCode}})",
|
||||
"manageCommands": "Quản lý quyền lệnh",
|
||||
"commandManagementDescription": "Quản lý quyền lệnh: Nhấp vào ✓ để cho phép tự động thực thi, ✗ để từ chối thực thi. Các mẫu có thể được bật/tắt hoặc xóa khỏi danh sách. <settingsLink>Xem tất cả cài đặt</settingsLink>",
|
||||
"addToAllowed": "Thêm vào danh sách cho phép",
|
||||
"removeFromAllowed": "Xóa khỏi danh sách cho phép",
|
||||
"addToDenied": "Thêm vào danh sách từ chối",
|
||||
"removeFromDenied": "Xóa khỏi danh sách từ chối",
|
||||
"abortCommand": "Hủy bỏ thực thi lệnh",
|
||||
"expandOutput": "Mở rộng kết quả",
|
||||
"collapseOutput": "Thu gọn kết quả",
|
||||
"expandManagement": "Mở rộng phần quản lý lệnh",
|
||||
"collapseManagement": "Thu gọn phần quản lý lệnh"
|
||||
},
|
||||
"response": "Phản hồi",
|
||||
"arguments": "Tham số",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
16
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo已查看此目录中使用的源代码定义名称(工作区外):"
|
||||
},
|
||||
"commandOutput": "命令输出",
|
||||
"commandExecution": {
|
||||
"running": "正在运行",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "已退出 ({{exitCode}})",
|
||||
"manageCommands": "管理命令权限",
|
||||
"commandManagementDescription": "管理命令权限:点击 ✓ 允许自动执行,点击 ✗ 拒绝执行。可以打开/关闭模式或从列表中删除。<settingsLink>查看所有设置</settingsLink>",
|
||||
"addToAllowed": "添加到允许列表",
|
||||
"removeFromAllowed": "从允许列表中删除",
|
||||
"addToDenied": "添加到拒绝列表",
|
||||
"removeFromDenied": "从拒绝列表中删除",
|
||||
"abortCommand": "中止命令执行",
|
||||
"expandOutput": "展开输出",
|
||||
"collapseOutput": "折叠输出",
|
||||
"expandManagement": "展开命令管理部分",
|
||||
"collapseManagement": "折叠命令管理部分"
|
||||
},
|
||||
"response": "响应",
|
||||
"arguments": "参数",
|
||||
"mcp": {
|
||||
|
|
|
|||
16
webview-ui/src/i18n/locales/zh-TW/chat.json
generated
16
webview-ui/src/i18n/locales/zh-TW/chat.json
generated
|
|
@ -192,6 +192,22 @@
|
|||
"didViewDefinitionsOutsideWorkspace": "Roo 已檢視此目錄(工作區外)中使用的原始碼定義名稱:"
|
||||
},
|
||||
"commandOutput": "命令輸出",
|
||||
"commandExecution": {
|
||||
"running": "正在執行",
|
||||
"pid": "PID: {{pid}}",
|
||||
"exited": "已退出 ({{exitCode}})",
|
||||
"manageCommands": "管理命令權限",
|
||||
"commandManagementDescription": "管理命令權限:點擊 ✓ 允許自動執行,點擊 ✗ 拒絕執行。可以開啟/關閉模式或從清單中刪除。<settingsLink>檢視所有設定</settingsLink>",
|
||||
"addToAllowed": "新增至允許清單",
|
||||
"removeFromAllowed": "從允許清單中移除",
|
||||
"addToDenied": "新增至拒絕清單",
|
||||
"removeFromDenied": "從拒絕清單中移除",
|
||||
"abortCommand": "中止命令執行",
|
||||
"expandOutput": "展開輸出",
|
||||
"collapseOutput": "折疊輸出",
|
||||
"expandManagement": "展開命令管理部分",
|
||||
"collapseManagement": "折疊命令管理部分"
|
||||
},
|
||||
"response": "回應",
|
||||
"arguments": "參數",
|
||||
"mcp": {
|
||||
|
|
|
|||
275
webview-ui/src/utils/__tests__/commandPatterns.spec.ts
Normal file
275
webview-ui/src/utils/__tests__/commandPatterns.spec.ts
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import { extractCommandPatterns, getPatternDescription, parseCommandAndOutput } from "../commandPatterns"
|
||||
|
||||
describe("extractCommandPatterns", () => {
|
||||
it("should extract simple command", () => {
|
||||
const patterns = extractCommandPatterns("ls")
|
||||
expect(patterns).toEqual(["ls"])
|
||||
})
|
||||
|
||||
it("should extract command with arguments", () => {
|
||||
const patterns = extractCommandPatterns("npm install express")
|
||||
expect(patterns).toEqual(["npm", "npm install", "npm install express"])
|
||||
})
|
||||
|
||||
it("should handle piped commands", () => {
|
||||
const patterns = extractCommandPatterns("ls -la | grep test")
|
||||
expect(patterns).toContain("ls")
|
||||
expect(patterns).toContain("grep")
|
||||
expect(patterns).toContain("grep test")
|
||||
})
|
||||
|
||||
it("should handle chained commands with &&", () => {
|
||||
const patterns = extractCommandPatterns("npm install && npm run build")
|
||||
expect(patterns).toContain("npm")
|
||||
expect(patterns).toContain("npm install")
|
||||
expect(patterns).toContain("npm run")
|
||||
expect(patterns).toContain("npm run build")
|
||||
})
|
||||
|
||||
it("should handle chained commands with ||", () => {
|
||||
const patterns = extractCommandPatterns("npm test || npm run test:ci")
|
||||
expect(patterns).toContain("npm")
|
||||
expect(patterns).toContain("npm test")
|
||||
expect(patterns).toContain("npm run")
|
||||
expect(patterns).toContain("npm run test:ci")
|
||||
})
|
||||
|
||||
it("should handle semicolon separated commands", () => {
|
||||
const patterns = extractCommandPatterns("cd src; npm install")
|
||||
expect(patterns).toContain("cd")
|
||||
expect(patterns).toContain("cd src")
|
||||
expect(patterns).toContain("npm")
|
||||
expect(patterns).toContain("npm install")
|
||||
})
|
||||
|
||||
it("should stop at flags", () => {
|
||||
const patterns = extractCommandPatterns('git commit -m "test message"')
|
||||
expect(patterns).toContain("git")
|
||||
expect(patterns).toContain("git commit")
|
||||
expect(patterns).not.toContain("git commit -m")
|
||||
})
|
||||
|
||||
it("should stop at paths with slashes", () => {
|
||||
const patterns = extractCommandPatterns("cd /usr/local/bin")
|
||||
expect(patterns).toContain("cd")
|
||||
expect(patterns).not.toContain("cd /usr/local/bin")
|
||||
})
|
||||
|
||||
it("should handle empty or null input", () => {
|
||||
expect(extractCommandPatterns("")).toEqual([])
|
||||
expect(extractCommandPatterns(" ")).toEqual([])
|
||||
expect(extractCommandPatterns(null as any)).toEqual([])
|
||||
expect(extractCommandPatterns(undefined as any)).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle complex command with multiple operators", () => {
|
||||
const patterns = extractCommandPatterns('npm install && npm test | grep success || echo "failed"')
|
||||
expect(patterns).toContain("npm")
|
||||
expect(patterns).toContain("npm install")
|
||||
expect(patterns).toContain("npm test")
|
||||
expect(patterns).toContain("grep")
|
||||
expect(patterns).toContain("grep success")
|
||||
expect(patterns).toContain("echo")
|
||||
})
|
||||
|
||||
it("should handle malformed commands gracefully", () => {
|
||||
const patterns = extractCommandPatterns("npm install && ")
|
||||
expect(patterns).toContain("npm")
|
||||
expect(patterns).toContain("npm install")
|
||||
})
|
||||
|
||||
it("should extract main command even if parsing fails", () => {
|
||||
// Create a command that might cause parsing issues
|
||||
const patterns = extractCommandPatterns('echo "unclosed quote')
|
||||
expect(patterns).toContain("echo")
|
||||
})
|
||||
|
||||
it("should handle commands with special characters in arguments", () => {
|
||||
const patterns = extractCommandPatterns("git add .")
|
||||
expect(patterns).toContain("git")
|
||||
expect(patterns).toContain("git add")
|
||||
expect(patterns).not.toContain("git add .")
|
||||
})
|
||||
|
||||
it("should return sorted patterns", () => {
|
||||
const patterns = extractCommandPatterns("npm run build && git push")
|
||||
expect(patterns).toEqual([...patterns].sort())
|
||||
})
|
||||
})
|
||||
|
||||
describe("getPatternDescription", () => {
|
||||
it("should return descriptions for common commands", () => {
|
||||
expect(getPatternDescription("cd")).toBe("directory navigation")
|
||||
expect(getPatternDescription("npm")).toBe("npm commands")
|
||||
expect(getPatternDescription("npm install")).toBe("npm install commands")
|
||||
expect(getPatternDescription("git")).toBe("git commands")
|
||||
expect(getPatternDescription("git push")).toBe("git push commands")
|
||||
expect(getPatternDescription("python")).toBe("python scripts")
|
||||
})
|
||||
|
||||
it("should return default description for unknown commands", () => {
|
||||
expect(getPatternDescription("unknowncommand")).toBe("unknowncommand commands")
|
||||
expect(getPatternDescription("custom-tool")).toBe("custom-tool commands")
|
||||
})
|
||||
|
||||
it("should handle package managers", () => {
|
||||
expect(getPatternDescription("yarn")).toBe("yarn commands")
|
||||
expect(getPatternDescription("pnpm")).toBe("pnpm commands")
|
||||
expect(getPatternDescription("bun")).toBe("bun scripts")
|
||||
})
|
||||
|
||||
it("should handle build tools", () => {
|
||||
expect(getPatternDescription("make")).toBe("build automation")
|
||||
expect(getPatternDescription("cmake")).toBe("CMake build system")
|
||||
expect(getPatternDescription("cargo")).toBe("Rust cargo commands")
|
||||
expect(getPatternDescription("go build")).toBe("go build commands")
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseCommandAndOutput", () => {
|
||||
it("should parse command with $ prefix", () => {
|
||||
const text = "$ npm install\nInstalling packages..."
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.command).toBe("npm install")
|
||||
expect(result.output).toBe("Installing packages...")
|
||||
})
|
||||
|
||||
it("should parse command with ❯ prefix", () => {
|
||||
const text = "❯ git status\nOn branch main"
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.command).toBe("git status")
|
||||
expect(result.output).toBe("On branch main")
|
||||
})
|
||||
|
||||
it("should parse command with > prefix", () => {
|
||||
const text = "> echo hello\nhello"
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.command).toBe("echo hello")
|
||||
expect(result.output).toBe("hello")
|
||||
})
|
||||
|
||||
it("should return original text if no command prefix found", () => {
|
||||
const text = "npm install"
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.command).toBe("npm install")
|
||||
expect(result.output).toBe("")
|
||||
})
|
||||
|
||||
it("should extract AI suggestions from output", () => {
|
||||
const text = "$ npm install\nSuggested patterns: npm, npm install, npm run"
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.suggestions).toEqual(["npm", "npm install", "npm run"])
|
||||
})
|
||||
|
||||
it("should extract suggestions with different formats", () => {
|
||||
const text = "$ git push\nCommand patterns: git, git push"
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.suggestions).toEqual(["git", "git push"])
|
||||
})
|
||||
|
||||
it('should extract suggestions from "you can allow" format', () => {
|
||||
const text = "$ docker run\nYou can allow: docker, docker run"
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.suggestions).toEqual(["docker", "docker run"])
|
||||
})
|
||||
|
||||
it("should extract suggestions from bullet points", () => {
|
||||
const text = `$ npm test
|
||||
Output here...
|
||||
- npm
|
||||
- npm test
|
||||
- npm run`
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.suggestions).toContain("npm")
|
||||
expect(result.suggestions).toContain("npm test")
|
||||
expect(result.suggestions).toContain("npm run")
|
||||
})
|
||||
|
||||
it("should extract suggestions from various bullet formats", () => {
|
||||
const text = `$ command
|
||||
• npm
|
||||
* git
|
||||
- docker
|
||||
▪ python`
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.suggestions).toContain("npm")
|
||||
expect(result.suggestions).toContain("git")
|
||||
expect(result.suggestions).toContain("docker")
|
||||
expect(result.suggestions).toContain("python")
|
||||
})
|
||||
|
||||
it("should extract suggestions with backticks", () => {
|
||||
const text = "$ npm install\n- `npm`\n- `npm install`"
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.suggestions).toContain("npm")
|
||||
expect(result.suggestions).toContain("npm install")
|
||||
})
|
||||
|
||||
it("should handle empty text", () => {
|
||||
const result = parseCommandAndOutput("")
|
||||
expect(result.command).toBe("")
|
||||
expect(result.output).toBe("")
|
||||
expect(result.suggestions).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle multiline commands", () => {
|
||||
const text = `$ npm install \\
|
||||
express \\
|
||||
mongoose
|
||||
Installing...`
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.command).toBe("npm install \\")
|
||||
expect(result.output).toContain("express")
|
||||
})
|
||||
|
||||
it("should include all suggestions from comma-separated list", () => {
|
||||
const text = "$ test\nSuggested patterns: npm, npm install, npm run"
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.suggestions).toEqual(["npm", "npm install", "npm run"])
|
||||
})
|
||||
|
||||
it("should handle case variations in suggestion patterns", () => {
|
||||
const text = "$ test\nSuggested Patterns: npm, git\nCommand Patterns: docker"
|
||||
const result = parseCommandAndOutput(text)
|
||||
// Now it should accumulate all suggestions
|
||||
expect(result.suggestions).toContain("npm")
|
||||
expect(result.suggestions).toContain("git")
|
||||
expect(result.suggestions).toContain("docker")
|
||||
})
|
||||
|
||||
it("should handle text already split by Output:", () => {
|
||||
const text = "npm install && cd backend\nOutput:\ngithub-pr-contributors-tracker@1.0.0 prepare"
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.command).toBe("npm install && cd backend")
|
||||
expect(result.output).toBe("github-pr-contributors-tracker@1.0.0 prepare")
|
||||
})
|
||||
|
||||
it("should preserve original command when Output: separator is present", () => {
|
||||
const text = "npm install\nOutput:\n$ npm install\nInstalling packages..."
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.command).toBe("npm install")
|
||||
expect(result.output).toBe("$ npm install\nInstalling packages...")
|
||||
})
|
||||
|
||||
it("should handle Output: separator with no output", () => {
|
||||
const text = "ls -la\nOutput:"
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.command).toBe("ls -la")
|
||||
expect(result.output).toBe("")
|
||||
})
|
||||
|
||||
it("should handle Output: separator with whitespace", () => {
|
||||
const text = "git status\nOutput: \n On branch main "
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.command).toBe("git status")
|
||||
expect(result.output).toBe("On branch main")
|
||||
})
|
||||
|
||||
it("should only use first Output: occurrence as separator", () => {
|
||||
const text = 'echo "test"\nOutput:\nFirst output\nOutput: Second output'
|
||||
const result = parseCommandAndOutput(text)
|
||||
expect(result.command).toBe('echo "test"')
|
||||
expect(result.output).toBe("First output\nOutput: Second output")
|
||||
})
|
||||
})
|
||||
197
webview-ui/src/utils/commandPatterns.ts
Normal file
197
webview-ui/src/utils/commandPatterns.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import { parse } from "shell-quote"
|
||||
|
||||
export interface CommandPattern {
|
||||
pattern: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function extractCommandPatterns(command: string): string[] {
|
||||
if (!command?.trim()) return []
|
||||
|
||||
const patterns = new Set<string>()
|
||||
|
||||
try {
|
||||
const parsed = parse(command)
|
||||
|
||||
const commandSeparators = new Set(["|", "&&", "||", ";"])
|
||||
let current: any[] = []
|
||||
|
||||
for (const token of parsed) {
|
||||
if (typeof token === "object" && "op" in token && token.op && commandSeparators.has(token.op)) {
|
||||
if (current.length) processCommand(current, patterns)
|
||||
current = []
|
||||
} else {
|
||||
current.push(token)
|
||||
}
|
||||
}
|
||||
|
||||
if (current.length) processCommand(current, patterns)
|
||||
} catch (_error) {
|
||||
// If parsing fails, try to extract at least the main command
|
||||
const mainCommand = command.trim().split(/\s+/)[0]
|
||||
if (mainCommand) patterns.add(mainCommand)
|
||||
}
|
||||
|
||||
return Array.from(patterns).sort()
|
||||
}
|
||||
|
||||
function processCommand(cmd: any[], patterns: Set<string>) {
|
||||
if (!cmd.length || typeof cmd[0] !== "string") return
|
||||
|
||||
const mainCmd = cmd[0]
|
||||
patterns.add(mainCmd)
|
||||
|
||||
// Patterns that indicate we should stop looking for subcommands
|
||||
const breakingExps = [/^-/, /[\\/.~ ]/]
|
||||
|
||||
// Build up patterns progressively
|
||||
for (let i = 1; i < cmd.length; i++) {
|
||||
const arg = cmd[i]
|
||||
if (typeof arg !== "string" || breakingExps.some((re) => re.test(arg))) break
|
||||
|
||||
const pattern = cmd.slice(0, i + 1).join(" ")
|
||||
patterns.add(pattern)
|
||||
}
|
||||
}
|
||||
|
||||
export function getPatternDescription(pattern: string): string {
|
||||
// Generate human-readable descriptions for common patterns
|
||||
const descriptions: Record<string, string> = {
|
||||
cd: "directory navigation",
|
||||
ls: "list directory contents",
|
||||
pwd: "print working directory",
|
||||
mkdir: "create directories",
|
||||
rm: "remove files/directories",
|
||||
cp: "copy files/directories",
|
||||
mv: "move/rename files",
|
||||
cat: "display file contents",
|
||||
echo: "display text",
|
||||
npm: "npm commands",
|
||||
"npm install": "npm install commands",
|
||||
"npm run": "all npm run scripts",
|
||||
"npm test": "npm test commands",
|
||||
"npm start": "npm start commands",
|
||||
"npm build": "npm build commands",
|
||||
yarn: "yarn commands",
|
||||
"yarn install": "yarn install commands",
|
||||
"yarn run": "all yarn run scripts",
|
||||
pnpm: "pnpm commands",
|
||||
"pnpm install": "pnpm install commands",
|
||||
"pnpm run": "all pnpm run scripts",
|
||||
git: "git commands",
|
||||
"git add": "git add commands",
|
||||
"git commit": "git commit commands",
|
||||
"git push": "git push commands",
|
||||
"git pull": "git pull commands",
|
||||
"git clone": "git clone commands",
|
||||
"git checkout": "git checkout commands",
|
||||
"git branch": "git branch commands",
|
||||
"git merge": "git merge commands",
|
||||
"git status": "git status commands",
|
||||
"git log": "git log commands",
|
||||
python: "python scripts",
|
||||
python3: "python3 scripts",
|
||||
node: "node.js scripts",
|
||||
deno: "deno scripts",
|
||||
bun: "bun scripts",
|
||||
docker: "docker commands",
|
||||
"docker run": "docker run commands",
|
||||
"docker build": "docker build commands",
|
||||
"docker compose": "docker compose commands",
|
||||
curl: "HTTP requests",
|
||||
wget: "download files",
|
||||
grep: "search text patterns",
|
||||
find: "find files/directories",
|
||||
sed: "stream editor",
|
||||
awk: "text processing",
|
||||
make: "build automation",
|
||||
cmake: "CMake build system",
|
||||
go: "go commands",
|
||||
"go run": "go run commands",
|
||||
"go build": "go build commands",
|
||||
"go test": "go test commands",
|
||||
cargo: "Rust cargo commands",
|
||||
"cargo run": "cargo run commands",
|
||||
"cargo build": "cargo build commands",
|
||||
"cargo test": "cargo test commands",
|
||||
dotnet: ".NET commands",
|
||||
"dotnet run": "dotnet run commands",
|
||||
"dotnet build": "dotnet build commands",
|
||||
"dotnet test": "dotnet test commands",
|
||||
}
|
||||
|
||||
return descriptions[pattern] || `${pattern} commands`
|
||||
}
|
||||
|
||||
export function parseCommandAndOutput(text: string): {
|
||||
command: string
|
||||
output: string
|
||||
suggestions: string[]
|
||||
} {
|
||||
// Default result
|
||||
const result = {
|
||||
command: text,
|
||||
output: "",
|
||||
suggestions: [] as string[],
|
||||
}
|
||||
|
||||
// First check if the text already has been split by COMMAND_OUTPUT_STRING
|
||||
// This happens when the command has already been executed and we have the output
|
||||
const outputSeparator = "Output:"
|
||||
const outputIndex = text.indexOf(outputSeparator)
|
||||
|
||||
if (outputIndex !== -1) {
|
||||
// Text is already split into command and output
|
||||
result.command = text.slice(0, outputIndex).trim()
|
||||
result.output = text.slice(outputIndex + outputSeparator.length).trim()
|
||||
} else {
|
||||
// Try to extract command from the text
|
||||
// Look for patterns like "$ command" or "❯ command" at the start
|
||||
const commandMatch = text.match(/^[$❯>]\s*(.+?)(?:\n|$)/m)
|
||||
if (commandMatch) {
|
||||
result.command = commandMatch[1].trim()
|
||||
result.output = text.substring(commandMatch.index! + commandMatch[0].length).trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Look for AI suggestions in the output
|
||||
// These might be in a format like:
|
||||
// "Suggested patterns: npm, npm install, npm run"
|
||||
// or as a list
|
||||
const suggestionPatterns = [
|
||||
/Suggested patterns?:\s*(.+?)(?:\n|$)/i,
|
||||
/Command patterns?:\s*(.+?)(?:\n|$)/i,
|
||||
/You (?:can|may|might) (?:want to )?(?:allow|add):\s*(.+?)(?:\n|$)/i,
|
||||
]
|
||||
|
||||
for (const pattern of suggestionPatterns) {
|
||||
const match = result.output.match(pattern)
|
||||
if (match) {
|
||||
// Split by common delimiters and clean up
|
||||
const suggestions = match[1]
|
||||
.split(/[,;]/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s) // Allow multi-word patterns like "npm install"
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
// Add to existing suggestions instead of replacing
|
||||
result.suggestions.push(...suggestions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
result.suggestions = Array.from(new Set(result.suggestions))
|
||||
|
||||
// Also look for bullet points or numbered lists
|
||||
// const listPattern = /^[\s\-*•·▪▫◦‣⁃]\s*`?([a-zA-Z0-9_-]+(?:\s+[a-zA-Z0-9_-]+)?)`?$/gm
|
||||
const lines = result.output.split("\n")
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^[\s\-*•·▪▫◦‣⁃]\s*`?([a-zA-Z0-9_-]+(?:\s+[a-zA-Z0-9_-]+)?)`?$/)
|
||||
if (match && match[1] && !result.suggestions.includes(match[1])) {
|
||||
result.suggestions.push(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue