feat: add run_slash_command tool for executing slash commands (#7473)

Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
roomote[bot] 2025-09-03 14:14:55 -04:00 • committed by GitHub
parent 130ce2936e
commit d7896924a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 972 additions and 0 deletions

View file

@ -11,6 +11,7 @@ export const experimentIds = [
"multiFileApplyDiff",
"preventFocusDisruption",
"imageGeneration",
"runSlashCommand",
] as const
export const experimentIdsSchema = z.enum(experimentIds)
@ -26,6 +27,7 @@ export const experimentsSchema = z.object({
multiFileApplyDiff: z.boolean().optional(),
preventFocusDisruption: z.boolean().optional(),
imageGeneration: z.boolean().optional(),
runSlashCommand: z.boolean().optional(),
})
export type Experiments = z.infer<typeof experimentsSchema>

View file

@ -34,6 +34,7 @@ export const toolNames = [
"fetch_instructions",
"codebase_search",
"update_todo_list",
"run_slash_command",
"generate_image",
] as const

View file

@ -28,6 +28,7 @@ import { attemptCompletionTool } from "../tools/attemptCompletionTool"
import { newTaskTool } from "../tools/newTaskTool"
import { updateTodoListTool } from "../tools/updateTodoListTool"
import { runSlashCommandTool } from "../tools/runSlashCommandTool"
import { generateImageTool } from "../tools/generateImageTool"
import { formatResponse } from "../prompts/responses"
@ -222,6 +223,8 @@ export async function presentAssistantMessage(cline: Task) {
const modeName = getModeBySlug(mode, customModes)?.name ?? mode
return `[${block.name} in ${modeName} mode: '${message}']`
}
case "run_slash_command":
return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
case "generate_image":
return `[${block.name} for '${block.params.path}']`
}
@ -549,6 +552,9 @@ export async function presentAssistantMessage(cline: Task) {
askFinishSubTaskApproval,
)
break
case "run_slash_command":
await runSlashCommandTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
break
case "generate_image":
await generateImageTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
break

View file

@ -25,6 +25,7 @@ import { getSwitchModeDescription } from "./switch-mode"
import { getNewTaskDescription } from "./new-task"
import { getCodebaseSearchDescription } from "./codebase-search"
import { getUpdateTodoListDescription } from "./update-todo-list"
import { getRunSlashCommandDescription } from "./run-slash-command"
import { getGenerateImageDescription } from "./generate-image"
import { CodeIndexManager } from "../../../services/code-index/manager"
@ -57,6 +58,7 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
apply_diff: (args) =>
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
update_todo_list: (args) => getUpdateTodoListDescription(args),
run_slash_command: () => getRunSlashCommandDescription(),
generate_image: (args) => getGenerateImageDescription(args),
}
@ -136,6 +138,11 @@ export function getToolDescriptionsForMode(
tools.delete("generate_image")
}
// Conditionally exclude run_slash_command if experiment is not enabled
if (!experiments?.runSlashCommand) {
tools.delete("run_slash_command")
}
// Map tool descriptions for allowed tools
const descriptions = Array.from(tools).map((toolName) => {
const descriptionFn = toolDescriptionMap[toolName]
@ -171,5 +178,6 @@ export {
getInsertContentDescription,
getSearchAndReplaceDescription,
getCodebaseSearchDescription,
getRunSlashCommandDescription,
getGenerateImageDescription,
}

View file

@ -0,0 +1,32 @@
/**
* Generates the run_slash_command tool description.
*/
export function getRunSlashCommandDescription(): string {
return `## run_slash_command
Description: Execute a slash command to get specific instructions or content. Slash commands are predefined templates that provide detailed guidance for common tasks.
Parameters:
- command: (required) The name of the slash command to execute (e.g., "init", "test", "deploy")
- args: (optional) Additional arguments or context to pass to the command
Usage:
<run_slash_command>
<command>command_name</command>
<args>optional arguments</args>
</run_slash_command>
Examples:
1. Running the init command to analyze a codebase:
<run_slash_command>
<command>init</command>
</run_slash_command>
2. Running a command with additional context:
<run_slash_command>
<command>test</command>
<args>focus on integration tests</args>
</run_slash_command>
The command content will be returned for you to execute or follow as instructions.`
}

View file

@ -0,0 +1,380 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { runSlashCommandTool } from "../runSlashCommandTool"
import { Task } from "../../task/Task"
import { formatResponse } from "../../prompts/responses"
import { getCommand, getCommandNames } from "../../../services/command/commands"
// Mock dependencies
vi.mock("../../../services/command/commands", () => ({
getCommand: vi.fn(),
getCommandNames: vi.fn(),
}))
describe("runSlashCommandTool", () => {
let mockTask: any
let mockAskApproval: any
let mockHandleError: any
let mockPushToolResult: any
let mockRemoveClosingTag: any
beforeEach(() => {
vi.clearAllMocks()
mockTask = {
consecutiveMistakeCount: 0,
recordToolError: vi.fn(),
sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing parameter error"),
ask: vi.fn(),
cwd: "/test/project",
providerRef: {
deref: vi.fn().mockReturnValue({
getState: vi.fn().mockResolvedValue({
experiments: {
runSlashCommand: true,
},
}),
}),
},
}
mockAskApproval = vi.fn().mockResolvedValue(true)
mockHandleError = vi.fn()
mockPushToolResult = vi.fn()
mockRemoveClosingTag = vi.fn((tag, text) => text || "")
})
it("should handle missing command parameter", async () => {
const block = {
type: "tool_use" as const,
name: "run_slash_command" as const,
params: {},
partial: false,
}
await runSlashCommandTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockTask.consecutiveMistakeCount).toBe(1)
expect(mockTask.recordToolError).toHaveBeenCalledWith("run_slash_command")
expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("run_slash_command", "command")
expect(mockPushToolResult).toHaveBeenCalledWith("Missing parameter error")
})
it("should handle command not found", async () => {
const block = {
type: "tool_use" as const,
name: "run_slash_command" as const,
params: {
command: "nonexistent",
},
partial: false,
}
vi.mocked(getCommand).mockResolvedValue(undefined)
vi.mocked(getCommandNames).mockResolvedValue(["init", "test", "deploy"])
await runSlashCommandTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockTask.recordToolError).toHaveBeenCalledWith("run_slash_command")
expect(mockPushToolResult).toHaveBeenCalledWith(
formatResponse.toolError("Command 'nonexistent' not found. Available commands: init, test, deploy"),
)
})
it("should handle user rejection", async () => {
const block = {
type: "tool_use" as const,
name: "run_slash_command" as const,
params: {
command: "init",
},
partial: false,
}
const mockCommand = {
name: "init",
content: "Initialize project",
source: "built-in" as const,
filePath: "<built-in:init>",
description: "Initialize the project",
}
vi.mocked(getCommand).mockResolvedValue(mockCommand)
mockAskApproval.mockResolvedValue(false)
await runSlashCommandTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockAskApproval).toHaveBeenCalled()
expect(mockPushToolResult).not.toHaveBeenCalled()
})
it("should successfully execute built-in command", async () => {
const block = {
type: "tool_use" as const,
name: "run_slash_command" as const,
params: {
command: "init",
},
partial: false,
}
const mockCommand = {
name: "init",
content: "Initialize project content here",
source: "built-in" as const,
filePath: "<built-in:init>",
description: "Analyze codebase and create AGENTS.md",
}
vi.mocked(getCommand).mockResolvedValue(mockCommand)
await runSlashCommandTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockAskApproval).toHaveBeenCalledWith(
"tool",
JSON.stringify({
tool: "runSlashCommand",
command: "init",
args: undefined,
source: "built-in",
description: "Analyze codebase and create AGENTS.md",
}),
)
expect(mockPushToolResult).toHaveBeenCalledWith(
`Command: /init
Description: Analyze codebase and create AGENTS.md
Source: built-in
--- Command Content ---
Initialize project content here`,
)
})
it("should successfully execute command with arguments", async () => {
const block = {
type: "tool_use" as const,
name: "run_slash_command" as const,
params: {
command: "test",
args: "focus on unit tests",
},
partial: false,
}
const mockCommand = {
name: "test",
content: "Run tests with specific focus",
source: "project" as const,
filePath: ".roo/commands/test.md",
description: "Run project tests",
argumentHint: "test type or focus area",
}
vi.mocked(getCommand).mockResolvedValue(mockCommand)
await runSlashCommandTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockPushToolResult).toHaveBeenCalledWith(
`Command: /test
Description: Run project tests
Argument hint: test type or focus area
Provided arguments: focus on unit tests
Source: project
--- Command Content ---
Run tests with specific focus`,
)
})
it("should handle global command", async () => {
const block = {
type: "tool_use" as const,
name: "run_slash_command" as const,
params: {
command: "deploy",
},
partial: false,
}
const mockCommand = {
name: "deploy",
content: "Deploy application to production",
source: "global" as const,
filePath: "~/.roo/commands/deploy.md",
}
vi.mocked(getCommand).mockResolvedValue(mockCommand)
await runSlashCommandTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockPushToolResult).toHaveBeenCalledWith(
`Command: /deploy
Source: global
--- Command Content ---
Deploy application to production`,
)
})
it("should handle partial block", async () => {
const block = {
type: "tool_use" as const,
name: "run_slash_command" as const,
params: {
command: "init",
},
partial: true,
}
await runSlashCommandTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockTask.ask).toHaveBeenCalledWith(
"tool",
JSON.stringify({
tool: "runSlashCommand",
command: "init",
args: "",
}),
true,
)
expect(mockPushToolResult).not.toHaveBeenCalled()
})
it("should handle errors during execution", async () => {
const block = {
type: "tool_use" as const,
name: "run_slash_command" as const,
params: {
command: "init",
},
partial: false,
}
const error = new Error("Test error")
vi.mocked(getCommand).mockRejectedValue(error)
await runSlashCommandTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockHandleError).toHaveBeenCalledWith("running slash command", error)
})
it("should handle empty available commands list", async () => {
const block = {
type: "tool_use" as const,
name: "run_slash_command" as const,
params: {
command: "nonexistent",
},
partial: false,
}
vi.mocked(getCommand).mockResolvedValue(undefined)
vi.mocked(getCommandNames).mockResolvedValue([])
await runSlashCommandTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockPushToolResult).toHaveBeenCalledWith(
formatResponse.toolError("Command 'nonexistent' not found. Available commands: (none)"),
)
})
it("should reset consecutive mistake count on valid command", async () => {
const block = {
type: "tool_use" as const,
name: "run_slash_command" as const,
params: {
command: "init",
},
partial: false,
}
mockTask.consecutiveMistakeCount = 5
const mockCommand = {
name: "init",
content: "Initialize project",
source: "built-in" as const,
filePath: "<built-in:init>",
}
vi.mocked(getCommand).mockResolvedValue(mockCommand)
await runSlashCommandTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockTask.consecutiveMistakeCount).toBe(0)
})
})

View file

@ -0,0 +1,108 @@
import { Task } from "../task/Task"
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
import { formatResponse } from "../prompts/responses"
import { getCommand, getCommandNames } from "../../services/command/commands"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
export async function runSlashCommandTool(
task: Task,
block: ToolUse,
askApproval: AskApproval,
handleError: HandleError,
pushToolResult: PushToolResult,
removeClosingTag: RemoveClosingTag,
) {
// Check if run slash command experiment is enabled
const provider = task.providerRef.deref()
const state = await provider?.getState()
const isRunSlashCommandEnabled = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.RUN_SLASH_COMMAND)
if (!isRunSlashCommandEnabled) {
pushToolResult(
formatResponse.toolError(
"Run slash command is an experimental feature that must be enabled in settings. Please enable 'Run Slash Command' in the Experimental Settings section.",
),
)
return
}
const commandName: string | undefined = block.params.command
const args: string | undefined = block.params.args
try {
if (block.partial) {
const partialMessage = JSON.stringify({
tool: "runSlashCommand",
command: removeClosingTag("command", commandName),
args: removeClosingTag("args", args),
})
await task.ask("tool", partialMessage, block.partial).catch(() => {})
return
} else {
if (!commandName) {
task.consecutiveMistakeCount++
task.recordToolError("run_slash_command")
pushToolResult(await task.sayAndCreateMissingParamError("run_slash_command", "command"))
return
}
task.consecutiveMistakeCount = 0
// Get the command from the commands service
const command = await getCommand(task.cwd, commandName)
if (!command) {
// Get available commands for error message
const availableCommands = await getCommandNames(task.cwd)
task.recordToolError("run_slash_command")
pushToolResult(
formatResponse.toolError(
`Command '${commandName}' not found. Available commands: ${availableCommands.join(", ") || "(none)"}`,
),
)
return
}
const toolMessage = JSON.stringify({
tool: "runSlashCommand",
command: commandName,
args: args,
source: command.source,
description: command.description,
})
const didApprove = await askApproval("tool", toolMessage)
if (!didApprove) {
return
}
// Build the result message
let result = `Command: /${commandName}`
if (command.description) {
result += `\nDescription: ${command.description}`
}
if (command.argumentHint) {
result += `\nArgument hint: ${command.argumentHint}`
}
if (args) {
result += `\nProvided arguments: ${args}`
}
result += `\nSource: ${command.source}`
result += `\n\n--- Command Content ---\n\n${command.content}`
// Return the command content as the tool result
pushToolResult(result)
return
}
} catch (error) {
await handleError("running slash command", error)
return
}
}

View file

@ -362,6 +362,7 @@ export interface ClineSayTool {
| "insertContent"
| "generateImage"
| "imageGenerated"
| "runSlashCommand"
path?: string
diff?: string
content?: string
@ -399,6 +400,11 @@ export interface ClineSayTool {
}>
question?: string
imageData?: string // Base64 encoded image data for generated images
// Properties for runSlashCommand tool
command?: string
args?: string
source?: string
description?: string
}
// Must keep in sync with system prompt.

View file

@ -30,6 +30,7 @@ describe("experiments", () => {
multiFileApplyDiff: false,
preventFocusDisruption: false,
imageGeneration: false,
runSlashCommand: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
})
@ -40,6 +41,7 @@ describe("experiments", () => {
multiFileApplyDiff: false,
preventFocusDisruption: false,
imageGeneration: false,
runSlashCommand: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
})
@ -50,6 +52,7 @@ describe("experiments", () => {
multiFileApplyDiff: false,
preventFocusDisruption: false,
imageGeneration: false,
runSlashCommand: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
})

View file

@ -5,6 +5,7 @@ export const EXPERIMENT_IDS = {
POWER_STEERING: "powerSteering",
PREVENT_FOCUS_DISRUPTION: "preventFocusDisruption",
IMAGE_GENERATION: "imageGeneration",
RUN_SLASH_COMMAND: "runSlashCommand",
} as const satisfies Record<string, ExperimentId>
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
@ -20,6 +21,7 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
POWER_STEERING: { enabled: false },
PREVENT_FOCUS_DISRUPTION: { enabled: false },
IMAGE_GENERATION: { enabled: false },
RUN_SLASH_COMMAND: { enabled: false },
}
export const experimentDefault = Object.fromEntries(

View file

@ -160,6 +160,11 @@ export interface NewTaskToolUse extends ToolUse {
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message" | "todos">>
}
export interface RunSlashCommandToolUse extends ToolUse {
name: "run_slash_command"
params: Partial<Pick<Record<ToolParamName, string>, "command" | "args">>
}
export interface SearchAndReplaceToolUse extends ToolUse {
name: "search_and_replace"
params: Required<Pick<Record<ToolParamName, string>, "path" | "search" | "replace">> &
@ -197,6 +202,7 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
search_and_replace: "search and replace",
codebase_search: "codebase search",
update_todo_list: "update todo list",
run_slash_command: "run slash command",
generate_image: "generate images",
} as const
@ -237,6 +243,7 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
"switch_mode",
"new_task",
"update_todo_list",
"run_slash_command",
] as const
export type DiffResult =

View file

@ -791,6 +791,75 @@ export const ChatRowContent = ({
</div>
</>
)
case "runSlashCommand": {
const slashCommandInfo = tool
return (
<>
<div style={headerStyle}>
{toolIcon("play")}
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? t("chat:slashCommand.wantsToRun")
: t("chat:slashCommand.didRun")}
</span>
</div>
<div
style={{
marginTop: "4px",
backgroundColor: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-editorGroup-border)",
borderRadius: "4px",
overflow: "hidden",
cursor: "pointer",
}}
onClick={handleToggleExpand}>
<ToolUseBlockHeader
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "10px 12px",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ fontWeight: "500", fontSize: "var(--vscode-font-size)" }}>
/{slashCommandInfo.command}
</span>
{slashCommandInfo.source && (
<VSCodeBadge style={{ fontSize: "calc(var(--vscode-font-size) - 2px)" }}>
{slashCommandInfo.source}
</VSCodeBadge>
)}
</div>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
</ToolUseBlockHeader>
{isExpanded && (slashCommandInfo.args || slashCommandInfo.description) && (
<div
style={{
padding: "12px 16px",
borderTop: "1px solid var(--vscode-editorGroup-border)",
display: "flex",
flexDirection: "column",
gap: "8px",
}}>
{slashCommandInfo.args && (
<div>
<span style={{ fontWeight: "500" }}>Arguments: </span>
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
{slashCommandInfo.args}
</span>
</div>
)}
{slashCommandInfo.description && (
<div style={{ color: "var(--vscode-descriptionForeground)" }}>
{slashCommandInfo.description}
</div>
)}
</div>
)}
</div>
</>
)
}
case "generateImage":
return (
<>
@ -1159,6 +1228,80 @@ export const ChatRowContent = ({
return <CodebaseSearchResultsDisplay results={results} />
case "user_edit_todos":
return <UpdateTodoListToolBlock userEdited onChange={() => {}} />
case "tool" as any:
// Handle say tool messages
const sayTool = safeJsonParse<ClineSayTool>(message.text)
if (!sayTool) return null
switch (sayTool.tool) {
case "runSlashCommand": {
const slashCommandInfo = sayTool
return (
<>
<div style={headerStyle}>
<span
className="codicon codicon-terminal-cmd"
style={{
color: "var(--vscode-foreground)",
marginBottom: "-1.5px",
}}></span>
<span style={{ fontWeight: "bold" }}>{t("chat:slashCommand.didRun")}</span>
</div>
<ToolUseBlock>
<ToolUseBlockHeader
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: "4px",
padding: "10px 12px",
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
width: "100%",
}}>
<span
style={{ fontWeight: "500", fontSize: "var(--vscode-font-size)" }}>
/{slashCommandInfo.command}
</span>
{slashCommandInfo.args && (
<span
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: "var(--vscode-font-size)",
}}>
{slashCommandInfo.args}
</span>
)}
</div>
{slashCommandInfo.description && (
<div
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: "calc(var(--vscode-font-size) - 1px)",
}}>
{slashCommandInfo.description}
</div>
)}
{slashCommandInfo.source && (
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<VSCodeBadge
style={{ fontSize: "calc(var(--vscode-font-size) - 2px)" }}>
{slashCommandInfo.source}
</VSCodeBadge>
</div>
)}
</ToolUseBlockHeader>
</ToolUseBlock>
</>
)
}
default:
return null
}
case "image":
// Parse the JSON to get imageUri and imagePath
const imageInfo = safeJsonParse<{ imageUri: string; imagePath: string }>(message.text || "{}")

View file

@ -949,6 +949,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
"listCodeDefinitionNames",
"searchFiles",
"codebaseSearch",
"runSlashCommand",
].includes(tool.tool)
}

View file

@ -0,0 +1,127 @@
import React from "react"
import { render } from "@/utils/test-utils"
import { describe, it, expect, beforeEach, vi } from "vitest"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"
import { ChatRowContent } from "../ChatRow"
// Mock i18n
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"chat:slashCommand.wantsToRun": "Roo wants to run slash command:",
"chat:slashCommand.didRun": "Roo ran slash command:",
}
return translations[key] || key
},
}),
Trans: ({ i18nKey, children }: { i18nKey: string; children?: React.ReactNode }) => {
return <>{children || i18nKey}</>
},
initReactI18next: {
type: "3rdParty",
init: () => {},
},
}))
// Mock VSCodeBadge
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
VSCodeBadge: ({ children, ...props }: { children: React.ReactNode }) => <span {...props}>{children}</span>,
}))
const queryClient = new QueryClient()
const renderChatRowWithProviders = (message: any, isExpanded = false) => {
return render(
<ExtensionStateContextProvider>
<QueryClientProvider client={queryClient}>
<ChatRowContent
message={message}
isExpanded={isExpanded}
isLast={false}
isStreaming={false}
onToggleExpand={mockOnToggleExpand}
onSuggestionClick={mockOnSuggestionClick}
onBatchFileResponse={mockOnBatchFileResponse}
onFollowUpUnmount={mockOnFollowUpUnmount}
isFollowUpAnswered={false}
/>
</QueryClientProvider>
</ExtensionStateContextProvider>,
)
}
const mockOnToggleExpand = vi.fn()
const mockOnSuggestionClick = vi.fn()
const mockOnBatchFileResponse = vi.fn()
const mockOnFollowUpUnmount = vi.fn()
describe("ChatRow - runSlashCommand tool", () => {
beforeEach(() => {
vi.clearAllMocks()
})
it("should display runSlashCommand ask message with command only", () => {
const message: any = {
type: "ask",
ask: "tool",
ts: Date.now(),
text: JSON.stringify({
tool: "runSlashCommand",
command: "init",
}),
partial: false,
}
const { getByText } = renderChatRowWithProviders(message)
expect(getByText("Roo wants to run slash command:")).toBeInTheDocument()
expect(getByText("/init")).toBeInTheDocument()
})
it("should display runSlashCommand ask message with command and args", () => {
const message: any = {
type: "ask",
ask: "tool",
ts: Date.now(),
text: JSON.stringify({
tool: "runSlashCommand",
command: "test",
args: "focus on unit tests",
description: "Run project tests",
source: "project",
}),
partial: false,
}
const { getByText } = renderChatRowWithProviders(message, true) // Pass true to expand
expect(getByText("Roo wants to run slash command:")).toBeInTheDocument()
expect(getByText("/test")).toBeInTheDocument()
expect(getByText("Arguments:")).toBeInTheDocument()
expect(getByText("focus on unit tests")).toBeInTheDocument()
expect(getByText("Run project tests")).toBeInTheDocument()
expect(getByText("project")).toBeInTheDocument()
})
it("should display runSlashCommand say message", () => {
const message: any = {
type: "say",
say: "tool",
ts: Date.now(),
text: JSON.stringify({
tool: "runSlashCommand",
command: "deploy",
source: "global",
}),
partial: false,
}
const { getByText } = renderChatRowWithProviders(message)
expect(getByText("Roo ran slash command:")).toBeInTheDocument()
expect(getByText("/deploy")).toBeInTheDocument()
expect(getByText("global")).toBeInTheDocument()
})
})

View file

@ -231,6 +231,7 @@ describe("mergeExtensionState", () => {
preventFocusDisruption: false,
newTaskRequireTodos: false,
imageGeneration: false,
runSlashCommand: false,
} as Record<ExperimentId, boolean>,
}
@ -250,6 +251,7 @@ describe("mergeExtensionState", () => {
preventFocusDisruption: false,
newTaskRequireTodos: false,
imageGeneration: false,
runSlashCommand: false,
})
})
})

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "Missatges en cua:",
"clickToEdit": "Feu clic per editar el missatge"
},
"slashCommand": {
"wantsToRun": "Roo vol executar una comanda slash:",
"didRun": "Roo ha executat una comanda slash:"
}
}

View file

@ -742,6 +742,10 @@
"modelSelectionDescription": "Selecciona el model per a la generació d'imatges",
"warningMissingKey": "⚠️ La clau API d'OpenRouter és necessària per a la generació d'imatges. Si us plau, configura-la a dalt.",
"successConfigured": "✓ La generació d'imatges està configurada i llesta per utilitzar"
},
"RUN_SLASH_COMMAND": {
"name": "Habilitar comandes de barra diagonal iniciades pel model",
"description": "Quan està habilitat, Roo pot executar les vostres comandes de barra diagonal per executar fluxos de treball."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "Warteschlange Nachrichten:",
"clickToEdit": "Klicken zum Bearbeiten der Nachricht"
},
"slashCommand": {
"wantsToRun": "Roo möchte einen Slash-Befehl ausführen:",
"didRun": "Roo hat einen Slash-Befehl ausgeführt:"
}
}

View file

@ -742,6 +742,10 @@
"modelSelectionDescription": "Wähle das Modell für die Bildgenerierung aus",
"warningMissingKey": "⚠️ OpenRouter API-Schlüssel ist für Bildgenerierung erforderlich. Bitte konfiguriere ihn oben.",
"successConfigured": "✓ Bildgenerierung ist konfiguriert und einsatzbereit"
},
"RUN_SLASH_COMMAND": {
"name": "Modellinitierte Slash-Befehle aktivieren",
"description": "Wenn aktiviert, kann Roo deine Slash-Befehle ausführen, um Workflows zu starten."
}
},
"promptCaching": {

View file

@ -381,6 +381,10 @@
"confirm": "Delete"
}
},
"slashCommand": {
"wantsToRun": "Roo wants to run a slash command:",
"didRun": "Roo ran a slash command:"
},
"queuedMessages": {
"title": "Queued Messages:",
"clickToEdit": "Click to edit message"

View file

@ -741,6 +741,10 @@
"modelSelectionDescription": "Select the model to use for image generation",
"warningMissingKey": "⚠️ OpenRouter API key is required for image generation. Please configure it above.",
"successConfigured": "✓ Image generation is configured and ready to use"
},
"RUN_SLASH_COMMAND": {
"name": "Enable model-initiated slash commands",
"description": "When enabled, Roo can run your slash commands to execute workflows."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "Mensajes en cola:",
"clickToEdit": "Haz clic para editar el mensaje"
},
"slashCommand": {
"wantsToRun": "Roo quiere ejecutar un comando slash:",
"didRun": "Roo ejecutó un comando slash:"
}
}

View file

@ -742,6 +742,10 @@
"modelSelectionDescription": "Selecciona el modelo para la generación de imágenes",
"warningMissingKey": "⚠️ La clave API de OpenRouter es requerida para la generación de imágenes. Por favor, configúrala arriba.",
"successConfigured": "✓ La generación de imágenes está configurada y lista para usar"
},
"RUN_SLASH_COMMAND": {
"name": "Habilitar comandos slash iniciados por el modelo",
"description": "Cuando está habilitado, Roo puede ejecutar tus comandos slash para ejecutar flujos de trabajo."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "Messages en file d'attente :",
"clickToEdit": "Cliquez pour modifier le message"
},
"slashCommand": {
"wantsToRun": "Roo veut exécuter une commande slash:",
"didRun": "Roo a exécuté une commande slash:"
}
}

View file

@ -742,6 +742,10 @@
"modelSelectionDescription": "Sélectionnez le modèle pour la génération d'images",
"warningMissingKey": "⚠️ Une clé API OpenRouter est requise pour la génération d'images. Veuillez la configurer ci-dessus.",
"successConfigured": "✓ La génération d'images est configurée et prête à utiliser"
},
"RUN_SLASH_COMMAND": {
"name": "Activer les commandes slash initiées par le modèle",
"description": "Lorsque activé, Roo peut exécuter tes commandes slash pour lancer des workflows."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "कतार में संदेश:",
"clickToEdit": "संदेश संपादित करने के लिए क्लिक करें"
},
"slashCommand": {
"wantsToRun": "Roo एक स्लैश कमांड चलाना चाहता है:",
"didRun": "Roo ने एक स्लैश कमांड चलाया:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "छवि निर्माण के लिए उपयोग करने वाला मॉडल चुनें",
"warningMissingKey": "⚠️ छवि निर्माण के लिए OpenRouter API कुंजी आवश्यक है। कृपया इसे ऊपर कॉन्फ़िगर करें।",
"successConfigured": "✓ छवि निर्माण कॉन्फ़िगर है और उपयोग के लिए तैयार है"
},
"RUN_SLASH_COMMAND": {
"name": "मॉडल द्वारा शुरू किए गए स्लैश कमांड सक्षम करें",
"description": "जब सक्षम होता है, Roo वर्कफ़्लो चलाने के लिए आपके स्लैश कमांड चला सकता है।"
}
},
"promptCaching": {

View file

@ -397,5 +397,9 @@
"queuedMessages": {
"title": "Pesan Antrian:",
"clickToEdit": "Klik untuk mengedit pesan"
},
"slashCommand": {
"wantsToRun": "Roo ingin menjalankan perintah slash:",
"didRun": "Roo telah menjalankan perintah slash:"
}
}

View file

@ -772,6 +772,10 @@
"modelSelectionDescription": "Pilih model untuk pembuatan gambar",
"warningMissingKey": "⚠️ Kunci API OpenRouter diperlukan untuk pembuatan gambar. Silakan konfigurasi di atas.",
"successConfigured": "✓ Pembuatan gambar dikonfigurasi dan siap digunakan"
},
"RUN_SLASH_COMMAND": {
"name": "Aktifkan perintah slash yang dimulai model",
"description": "Ketika diaktifkan, Roo dapat menjalankan perintah slash Anda untuk mengeksekusi alur kerja."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "Messaggi in coda:",
"clickToEdit": "Clicca per modificare il messaggio"
},
"slashCommand": {
"wantsToRun": "Roo vuole eseguire un comando slash:",
"didRun": "Roo ha eseguito un comando slash:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "Seleziona il modello per la generazione di immagini",
"warningMissingKey": "⚠️ La chiave API OpenRouter è richiesta per la generazione di immagini. Configurala sopra.",
"successConfigured": "✓ La generazione di immagini è configurata e pronta per l'uso"
},
"RUN_SLASH_COMMAND": {
"name": "Abilita comandi slash avviati dal modello",
"description": "Quando abilitato, Roo può eseguire i tuoi comandi slash per eseguire flussi di lavoro."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "キューメッセージ:",
"clickToEdit": "クリックしてメッセージを編集"
},
"slashCommand": {
"wantsToRun": "Rooはスラッシュコマンドを実行したい:",
"didRun": "Rooはスラッシュコマンドを実行しました:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "画像生成に使用するモデルを選択",
"warningMissingKey": "⚠️ 画像生成にはOpenRouter APIキーが必要です。上記で設定してください。",
"successConfigured": "✓ 画像生成が設定され、使用準備完了です"
},
"RUN_SLASH_COMMAND": {
"name": "モデル開始スラッシュコマンドを有効にする",
"description": "有効にすると、Rooがワークフローを実行するためにあなたのスラッシュコマンドを実行できます。"
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "대기열 메시지:",
"clickToEdit": "클릭하여 메시지 편집"
},
"slashCommand": {
"wantsToRun": "Roo가 슬래시 명령어를 실행하려고 합니다:",
"didRun": "Roo가 슬래시 명령어를 실행했습니다:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "이미지 생성에 사용할 모델을 선택하세요",
"warningMissingKey": "⚠️ 이미지 생성에는 OpenRouter API 키가 필요합니다. 위에서 설정해주세요.",
"successConfigured": "✓ 이미지 생성이 구성되었으며 사용할 준비가 되었습니다"
},
"RUN_SLASH_COMMAND": {
"name": "모델 시작 슬래시 명령 활성화",
"description": "활성화되면 Roo가 워크플로를 실행하기 위해 슬래시 명령을 실행할 수 있습니다."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "Berichten in wachtrij:",
"clickToEdit": "Klik om bericht te bewerken"
},
"slashCommand": {
"wantsToRun": "Roo wil een slash commando uitvoeren:",
"didRun": "Roo heeft een slash commando uitgevoerd:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "Selecteer het model voor afbeeldingsgeneratie",
"warningMissingKey": "⚠️ OpenRouter API-sleutel is vereist voor afbeeldingsgeneratie. Configureer deze hierboven.",
"successConfigured": "✓ Afbeeldingsgeneratie is geconfigureerd en klaar voor gebruik"
},
"RUN_SLASH_COMMAND": {
"name": "Model-geïnitieerde slash-commando's inschakelen",
"description": "Wanneer ingeschakeld, kan Roo je slash-commando's uitvoeren om workflows uit te voeren."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "Wiadomości w kolejce:",
"clickToEdit": "Kliknij, aby edytować wiadomość"
},
"slashCommand": {
"wantsToRun": "Roo chce uruchomić komendę slash:",
"didRun": "Roo uruchomił komendę slash:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "Wybierz model do generowania obrazów",
"warningMissingKey": "⚠️ Klucz API OpenRouter jest wymagany do generowania obrazów. Skonfiguruj go powyżej.",
"successConfigured": "✓ Generowanie obrazów jest skonfigurowane i gotowe do użycia"
},
"RUN_SLASH_COMMAND": {
"name": "Włącz polecenia slash inicjowane przez model",
"description": "Gdy włączone, Roo może uruchamiać twoje polecenia slash w celu wykonywania przepływów pracy."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "Mensagens na fila:",
"clickToEdit": "Clique para editar a mensagem"
},
"slashCommand": {
"wantsToRun": "Roo quer executar um comando slash:",
"didRun": "Roo executou um comando slash:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "Selecione o modelo para geração de imagens",
"warningMissingKey": "⚠️ A chave de API do OpenRouter é necessária para geração de imagens. Configure-a acima.",
"successConfigured": "✓ A geração de imagens está configurada e pronta para uso"
},
"RUN_SLASH_COMMAND": {
"name": "Ativar comandos slash iniciados pelo modelo",
"description": "Quando ativado, Roo pode executar seus comandos slash para executar fluxos de trabalho."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "Сообщения в очереди:",
"clickToEdit": "Нажмите, чтобы редактировать сообщение"
},
"slashCommand": {
"wantsToRun": "Roo хочет выполнить слеш-команду:",
"didRun": "Roo выполнил слеш-команду:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "Выберите модель для генерации изображений",
"warningMissingKey": "⚠️ API-ключ OpenRouter необходим для генерации изображений. Настройте его выше.",
"successConfigured": "✓ Генерация изображений настроена и готова к использованию"
},
"RUN_SLASH_COMMAND": {
"name": "Включить слэш-команды, инициированные моделью",
"description": "Когда включено, Roo может выполнять ваши слэш-команды для выполнения рабочих процессов."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "Sıradaki Mesajlar:",
"clickToEdit": "Mesajı düzenlemek için tıkla"
},
"slashCommand": {
"wantsToRun": "Roo bir slash komutu çalıştırmak istiyor:",
"didRun": "Roo bir slash komutu çalıştırdı:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "Görüntü üretimi için kullanılacak modeli seçin",
"warningMissingKey": "⚠️ Görüntü üretimi için OpenRouter API anahtarı gereklidir. Lütfen yukarıda yapılandırın.",
"successConfigured": "✓ Görüntü üretimi yapılandırılmış ve kullanıma hazır"
},
"RUN_SLASH_COMMAND": {
"name": "Model tarafından başlatılan slash komutlarını etkinleştir",
"description": "Etkinleştirildiğinde, Roo iş akışlarını yürütmek için slash komutlarınızı çalıştırabilir."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "Tin nhắn trong hàng đợi:",
"clickToEdit": "Nhấp để chỉnh sửa tin nhắn"
},
"slashCommand": {
"wantsToRun": "Roo muốn chạy lệnh slash:",
"didRun": "Roo đã chạy lệnh slash:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "Chọn mô hình để sử dụng cho việc tạo hình ảnh",
"warningMissingKey": "⚠️ Khóa API OpenRouter là bắt buộc để tạo hình ảnh. Vui lòng cấu hình ở trên.",
"successConfigured": "✓ Tạo hình ảnh đã được cấu hình và sẵn sàng sử dụng"
},
"RUN_SLASH_COMMAND": {
"name": "Bật lệnh slash do mô hình khởi tạo",
"description": "Khi được bật, Roo có thể chạy các lệnh slash của bạn để thực hiện các quy trình làm việc."
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "队列消息:",
"clickToEdit": "点击编辑消息"
},
"slashCommand": {
"wantsToRun": "Roo 想要运行斜杠命令:",
"didRun": "Roo 运行了斜杠命令:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "选择用于图像生成的模型",
"warningMissingKey": "⚠️ 图像生成需要 OpenRouter API 密钥。请在上方配置。",
"successConfigured": "✓ 图像生成已配置完成,可以使用"
},
"RUN_SLASH_COMMAND": {
"name": "启用模型发起的斜杠命令",
"description": "启用后 Roo 可运行斜杠命令执行工作流程。"
}
},
"promptCaching": {

View file

@ -391,5 +391,9 @@
"queuedMessages": {
"title": "佇列中的訊息:",
"clickToEdit": "點選以編輯訊息"
},
"slashCommand": {
"wantsToRun": "Roo 想要執行斜線指令:",
"didRun": "Roo 執行了斜線指令:"
}
}

View file

@ -743,6 +743,10 @@
"modelSelectionDescription": "選擇用於圖像生成的模型",
"warningMissingKey": "⚠️ 圖像生成需要 OpenRouter API 金鑰。請在上方設定。",
"successConfigured": "✓ 圖像生成已設定完成並準備使用"
},
"RUN_SLASH_COMMAND": {
"name": "啟用模型啟動的斜線命令",
"description": "啟用時,Roo 可以執行您的斜線命令來執行工作流程。"
}
},
"promptCaching": {