diff --git a/webview-ui/src/components/chat/CommandExecution.tsx b/webview-ui/src/components/chat/CommandExecution.tsx index 8c92ec7e7b..a2b63ec5bf 100644 --- a/webview-ui/src/components/chat/CommandExecution.tsx +++ b/webview-ui/src/components/chat/CommandExecution.tsx @@ -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(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(() => { + 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 -
- - +
+
+ + +
+ {showSuggestions && commandPatterns.length > 0 && ( + + )}
) diff --git a/webview-ui/src/components/chat/CommandPatternSelector.tsx b/webview-ui/src/components/chat/CommandPatternSelector.tsx new file mode 100644 index 0000000000..17799a8aec --- /dev/null +++ b/webview-ui/src/components/chat/CommandPatternSelector.tsx @@ -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 = ({ + 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 ( +
+ + + {isExpanded && ( +
+ {patterns.map((item, index) => { + const status = getPatternStatus(item.pattern) + return ( +
+
+ {item.pattern} + {item.description && ( + + - {item.description} + + )} +
+
+ + +
+
+ ) + })} +
+ )} +
+ ) +} diff --git a/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx b/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx new file mode 100644 index 0000000000..eafbde59aa --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/CommandExecution.spec.tsx @@ -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 }) =>
{source}
, +})) + +vi.mock("../CommandPatternSelector", () => ({ + CommandPatternSelector: ({ patterns, onAllowPatternChange, onDenyPatternChange }: any) => ( +
+ {patterns.map((p: any, i: number) => ( +
+ {p.pattern} + + +
+ ))} +
+ ), +})) + +// Mock ExtensionStateContext +const mockExtensionState = { + terminalShellIntegrationDisabled: false, + allowedCommands: ["npm"], + deniedCommands: ["rm"], + setAllowedCommands: vi.fn(), + setDeniedCommands: vi.fn(), +} + +const ExtensionStateWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +) + +describe("CommandExecution", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("should render command without output", () => { + render( + + + , + ) + + expect(screen.getByTestId("code-block")).toHaveTextContent("npm install") + }) + + it("should render command with output", () => { + render( + + + , + ) + + const codeBlocks = screen.getAllByTestId("code-block") + expect(codeBlocks[0]).toHaveTextContent("npm install") + }) + + it("should render with custom icon and title", () => { + const icon = 📦 + const title = Installing Dependencies + + render( + + + , + ) + + expect(screen.getByTestId("custom-icon")).toBeInTheDocument() + expect(screen.getByTestId("custom-title")).toBeInTheDocument() + }) + + it("should show command pattern selector for simple commands", () => { + render( + + + , + ) + + 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( + + + , + ) + + 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( + + + , + ) + + 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( + + + , + ) + + 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( + + + , + ) + + 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( + + + , + ) + + expect(screen.getByTestId("code-block")).toHaveTextContent("npm install") + }) + + it("should parse command with AI suggestions", () => { + render( + + + , + ) + + 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( + + + , + ) + + expect(screen.getByTestId("command-pattern-selector")).toBeInTheDocument() + expect(screen.getByText("ls")).toBeInTheDocument() + expect(screen.getByText("grep")).toBeInTheDocument() + }) + + it("should handle commands with && operator", () => { + render( + + + , + ) + + 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( + + + , + ) + + expect(screen.queryByTestId("command-pattern-selector")).not.toBeInTheDocument() + }) + + it("should expand output when terminal shell integration is disabled", () => { + const disabledState = { + ...mockExtensionState, + terminalShellIntegrationDisabled: true, + } + + render( + + + , + ) + + // 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( + + + , + ) + + expect(screen.getByTestId("command-pattern-selector")).toBeInTheDocument() + }) + + it("should handle pattern change when moving from denied to allowed", () => { + render( + + + , + ) + + 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: [] }) + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/CommandPatternSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/CommandPatternSelector.spec.tsx new file mode 100644 index 0000000000..4dd69e3969 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/CommandPatternSelector.spec.tsx @@ -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 ( + + Manage command permissions: Click ✓ to allow auto-execution, ✗ to deny execution. Patterns can be + toggled on/off or removed from lists. {components.settingsLink} + + ) + } + return {i18nKey} + }, +})) + +// Mock VSCodeLink +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeLink: ({ children, onClick }: any) => ( + + {children || "View all settings"} + + ), +})) + +// Mock StandardTooltip +vi.mock("../../ui/standard-tooltip", () => ({ + StandardTooltip: ({ children, content }: any) => ( +
+ {children} + {/* Render the content to make it testable */} +
{content}
+
+ ), +})) + +// 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() + + expect(screen.getByText("chat:commandExecution.manageCommands")).toBeInTheDocument() + expect(screen.queryByText("npm commands")).not.toBeInTheDocument() + }) + + it("should expand when clicked", () => { + render() + + 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() + + 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() + + 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() + + 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() + + 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() + + 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() + + 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() + + // 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() + + 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() + + 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() + + // 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() + + // 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") + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 01d9ee1c1a..17fe22d71e 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -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. Mostra tots els paràmetres", + "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": { diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 032145234d..2f603a3717 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -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. Alle Einstellungen anzeigen", + "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": { diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 3bbb3fbf72..d09c75424d 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -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. View all settings", + "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": { diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index adcfd1d40c..97dd41d952 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -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. Ver todos los ajustes", + "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": { diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 3e49a64867..1ecbeeca81 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -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. Voir tous les paramètres", + "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": { diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 3b5c7b8a67..48b9982172 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -192,6 +192,22 @@ "didViewDefinitionsOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में उपयोग किए गए सोर्स कोड परिभाषा नामों को देखा:" }, "commandOutput": "कमांड आउटपुट", + "commandExecution": { + "running": "चलाया जा रहा है", + "pid": "पीआईडी: {{pid}}", + "exited": "बाहर निकल गया ({{exitCode}})", + "manageCommands": "कमांड अनुमतियाँ प्रबंधित करें", + "commandManagementDescription": "कमांड अनुमतियों का प्रबंधन करें: स्वतः-निष्पादन की अनुमति देने के लिए ✓ पर क्लिक करें, निष्पादन से इनकार करने के लिए ✗ पर क्लिक करें। पैटर्न को चालू/बंद किया जा सकता है या सूचियों से हटाया जा सकता है। सभी सेटिंग्स देखें", + "addToAllowed": "अनुमत सूची में जोड़ें", + "removeFromAllowed": "अनुमत सूची से हटाएं", + "addToDenied": "अस्वीकृत सूची में जोड़ें", + "removeFromDenied": "अस्वीकृत सूची से हटाएं", + "abortCommand": "कमांड निष्पादन रद्द करें", + "expandOutput": "आउटपुट का विस्तार करें", + "collapseOutput": "आउटपुट संक्षिप्त करें", + "expandManagement": "कमांड प्रबंधन अनुभाग का विस्तार करें", + "collapseManagement": "कमांड प्रबंधन अनुभाग संक्षिप्त करें" + }, "response": "प्रतिक्रिया", "arguments": "आर्ग्युमेंट्स", "mcp": { diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 2ef1cb75e7..ba20ad324b 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -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. Lihat semua pengaturan", + "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": { diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index eb3984f1be..55691c71ac 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -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. Visualizza tutte le impostazioni", + "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": { diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 2f6e6bfab7..d502103260 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -192,6 +192,22 @@ "didViewDefinitionsOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)で使用されているソースコード定義名を表示しました:" }, "commandOutput": "コマンド出力", + "commandExecution": { + "running": "実行中", + "pid": "PID: {{pid}}", + "exited": "終了しました ({{exitCode}})", + "manageCommands": "コマンド権限の管理", + "commandManagementDescription": "コマンドの権限を管理します:✓ をクリックして自動実行を許可し、✗ をクリックして実行を拒否します。パターンはオン/オフの切り替えやリストからの削除が可能です。すべての設定を表示", + "addToAllowed": "許可リストに追加", + "removeFromAllowed": "許可リストから削除", + "addToDenied": "拒否リストに追加", + "removeFromDenied": "拒否リストから削除", + "abortCommand": "コマンドの実行を中止", + "expandOutput": "出力を展開", + "collapseOutput": "出力を折りたたむ", + "expandManagement": "コマンド管理セクションを展開", + "collapseManagement": "コマンド管理セクションを折りたたむ" + }, "response": "応答", "arguments": "引数", "mcp": { diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index f4a5c33602..2fcac36cba 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -192,6 +192,22 @@ "didViewDefinitionsOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)에서 사용된 소스 코드 정의 이름을 보았습니다:" }, "commandOutput": "명령 출력", + "commandExecution": { + "running": "실행 중", + "pid": "PID: {{pid}}", + "exited": "종료됨 ({{exitCode}})", + "manageCommands": "명령 권한 관리", + "commandManagementDescription": "명령 권한 관리: 자동 실행을 허용하려면 ✓를 클릭하고 실행을 거부하려면 ✗를 클릭하십시오. 패턴은 켜거나 끄거나 목록에서 제거할 수 있습니다. 모든 설정 보기", + "addToAllowed": "허용 목록에 추가", + "removeFromAllowed": "허용 목록에서 제거", + "addToDenied": "거부 목록에 추가", + "removeFromDenied": "거부 목록에서 제거", + "abortCommand": "명령 실행 중단", + "expandOutput": "출력 확장", + "collapseOutput": "출력 축소", + "expandManagement": "명령 관리 섹션 확장", + "collapseManagement": "명령 관리 섹션 축소" + }, "response": "응답", "arguments": "인수", "mcp": { diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 1d11db2668..7f295a0b3d 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -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. Bekijk alle instellingen", + "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": { diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 88c58418ad..807a2da644 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -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. Zobacz wszystkie ustawienia", + "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": { diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 3784d6cc64..e73eabfb93 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -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. Ver todas as configurações", + "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": { diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 0660d3e1d6..01987bdda1 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -187,6 +187,22 @@ "didViewDefinitionsOutsideWorkspace": "Roo просмотрел имена определений исходного кода в этой директории (вне рабочего пространства):" }, "commandOutput": "Вывод команды", + "commandExecution": { + "running": "Выполняется", + "pid": "PID: {{pid}}", + "exited": "Завершено ({{exitCode}})", + "manageCommands": "Управление разрешениями команд", + "commandManagementDescription": "Управляйте разрешениями команд: Нажмите ✓, чтобы разрешить автоматическое выполнение, ✗, чтобы запретить выполнение. Шаблоны можно включать/выключать или удалять из списков. Просмотреть все настройки", + "addToAllowed": "Добавить в список разрешенных", + "removeFromAllowed": "Удалить из списка разрешенных", + "addToDenied": "Добавить в список запрещенных", + "removeFromDenied": "Удалить из списка запрещенных", + "abortCommand": "Прервать выполнение команды", + "expandOutput": "Развернуть вывод", + "collapseOutput": "Свернуть вывод", + "expandManagement": "Развернуть раздел управления командами", + "collapseManagement": "Свернуть раздел управления командами" + }, "response": "Ответ", "arguments": "Аргументы", "mcp": { diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 75bc126dff..88e5fea67e 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -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. Tüm ayarları görüntüle", + "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": { diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 944eabcb94..a0562a85de 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -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. Xem tất cả cài đặt", + "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": { diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 616cd14fec..fe5c0bc768 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -192,6 +192,22 @@ "didViewDefinitionsOutsideWorkspace": "Roo已查看此目录中使用的源代码定义名称(工作区外):" }, "commandOutput": "命令输出", + "commandExecution": { + "running": "正在运行", + "pid": "PID: {{pid}}", + "exited": "已退出 ({{exitCode}})", + "manageCommands": "管理命令权限", + "commandManagementDescription": "管理命令权限:点击 ✓ 允许自动执行,点击 ✗ 拒绝执行。可以打开/关闭模式或从列表中删除。查看所有设置", + "addToAllowed": "添加到允许列表", + "removeFromAllowed": "从允许列表中删除", + "addToDenied": "添加到拒绝列表", + "removeFromDenied": "从拒绝列表中删除", + "abortCommand": "中止命令执行", + "expandOutput": "展开输出", + "collapseOutput": "折叠输出", + "expandManagement": "展开命令管理部分", + "collapseManagement": "折叠命令管理部分" + }, "response": "响应", "arguments": "参数", "mcp": { diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 662421900e..8623ad2b0e 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -192,6 +192,22 @@ "didViewDefinitionsOutsideWorkspace": "Roo 已檢視此目錄(工作區外)中使用的原始碼定義名稱:" }, "commandOutput": "命令輸出", + "commandExecution": { + "running": "正在執行", + "pid": "PID: {{pid}}", + "exited": "已退出 ({{exitCode}})", + "manageCommands": "管理命令權限", + "commandManagementDescription": "管理命令權限:點擊 ✓ 允許自動執行,點擊 ✗ 拒絕執行。可以開啟/關閉模式或從清單中刪除。檢視所有設定", + "addToAllowed": "新增至允許清單", + "removeFromAllowed": "從允許清單中移除", + "addToDenied": "新增至拒絕清單", + "removeFromDenied": "從拒絕清單中移除", + "abortCommand": "中止命令執行", + "expandOutput": "展開輸出", + "collapseOutput": "折疊輸出", + "expandManagement": "展開命令管理部分", + "collapseManagement": "折疊命令管理部分" + }, "response": "回應", "arguments": "參數", "mcp": { diff --git a/webview-ui/src/utils/__tests__/commandPatterns.spec.ts b/webview-ui/src/utils/__tests__/commandPatterns.spec.ts new file mode 100644 index 0000000000..2c16ab68a7 --- /dev/null +++ b/webview-ui/src/utils/__tests__/commandPatterns.spec.ts @@ -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") + }) +}) diff --git a/webview-ui/src/utils/commandPatterns.ts b/webview-ui/src/utils/commandPatterns.ts new file mode 100644 index 0000000000..f6c2b48294 --- /dev/null +++ b/webview-ui/src/utils/commandPatterns.ts @@ -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() + + 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) { + 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 = { + 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 +}