Преглед на файлове

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

Co-authored-by: Roo Code <[email protected]>
Co-authored-by: Matt Rubens <[email protected]>
Co-authored-by: Daniel Riccio <[email protected]>
roomote[bot] преди 4 месеца
родител
ревизия
d7896924a9
променени са 51 файла, в които са добавени 972 реда и са изтрити 0 реда
  1. 2 0
      packages/types/src/experiment.ts
  2. 1 0
      packages/types/src/tool.ts
  3. 6 0
      src/core/assistant-message/presentAssistantMessage.ts
  4. 8 0
      src/core/prompts/tools/index.ts
  5. 32 0
      src/core/prompts/tools/run-slash-command.ts
  6. 380 0
      src/core/tools/__tests__/runSlashCommandTool.spec.ts
  7. 108 0
      src/core/tools/runSlashCommandTool.ts
  8. 6 0
      src/shared/ExtensionMessage.ts
  9. 3 0
      src/shared/__tests__/experiments.spec.ts
  10. 2 0
      src/shared/experiments.ts
  11. 7 0
      src/shared/tools.ts
  12. 143 0
      webview-ui/src/components/chat/ChatRow.tsx
  13. 1 0
      webview-ui/src/components/chat/ChatView.tsx
  14. 127 0
      webview-ui/src/components/chat/__tests__/ChatRow.run-slash-command.spec.tsx
  15. 2 0
      webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx
  16. 4 0
      webview-ui/src/i18n/locales/ca/chat.json
  17. 4 0
      webview-ui/src/i18n/locales/ca/settings.json
  18. 4 0
      webview-ui/src/i18n/locales/de/chat.json
  19. 4 0
      webview-ui/src/i18n/locales/de/settings.json
  20. 4 0
      webview-ui/src/i18n/locales/en/chat.json
  21. 4 0
      webview-ui/src/i18n/locales/en/settings.json
  22. 4 0
      webview-ui/src/i18n/locales/es/chat.json
  23. 4 0
      webview-ui/src/i18n/locales/es/settings.json
  24. 4 0
      webview-ui/src/i18n/locales/fr/chat.json
  25. 4 0
      webview-ui/src/i18n/locales/fr/settings.json
  26. 4 0
      webview-ui/src/i18n/locales/hi/chat.json
  27. 4 0
      webview-ui/src/i18n/locales/hi/settings.json
  28. 4 0
      webview-ui/src/i18n/locales/id/chat.json
  29. 4 0
      webview-ui/src/i18n/locales/id/settings.json
  30. 4 0
      webview-ui/src/i18n/locales/it/chat.json
  31. 4 0
      webview-ui/src/i18n/locales/it/settings.json
  32. 4 0
      webview-ui/src/i18n/locales/ja/chat.json
  33. 4 0
      webview-ui/src/i18n/locales/ja/settings.json
  34. 4 0
      webview-ui/src/i18n/locales/ko/chat.json
  35. 4 0
      webview-ui/src/i18n/locales/ko/settings.json
  36. 4 0
      webview-ui/src/i18n/locales/nl/chat.json
  37. 4 0
      webview-ui/src/i18n/locales/nl/settings.json
  38. 4 0
      webview-ui/src/i18n/locales/pl/chat.json
  39. 4 0
      webview-ui/src/i18n/locales/pl/settings.json
  40. 4 0
      webview-ui/src/i18n/locales/pt-BR/chat.json
  41. 4 0
      webview-ui/src/i18n/locales/pt-BR/settings.json
  42. 4 0
      webview-ui/src/i18n/locales/ru/chat.json
  43. 4 0
      webview-ui/src/i18n/locales/ru/settings.json
  44. 4 0
      webview-ui/src/i18n/locales/tr/chat.json
  45. 4 0
      webview-ui/src/i18n/locales/tr/settings.json
  46. 4 0
      webview-ui/src/i18n/locales/vi/chat.json
  47. 4 0
      webview-ui/src/i18n/locales/vi/settings.json
  48. 4 0
      webview-ui/src/i18n/locales/zh-CN/chat.json
  49. 4 0
      webview-ui/src/i18n/locales/zh-CN/settings.json
  50. 4 0
      webview-ui/src/i18n/locales/zh-TW/chat.json
  51. 4 0
      webview-ui/src/i18n/locales/zh-TW/settings.json

+ 2 - 0
packages/types/src/experiment.ts

@@ -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>

+ 1 - 0
packages/types/src/tool.ts

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

+ 6 - 0
src/core/assistant-message/presentAssistantMessage.ts

@@ -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

+ 8 - 0
src/core/prompts/tools/index.ts

@@ -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,
 }

+ 32 - 0
src/core/prompts/tools/run-slash-command.ts

@@ -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.`
+}

+ 380 - 0
src/core/tools/__tests__/runSlashCommandTool.spec.ts

@@ -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)
+	})
+})

+ 108 - 0
src/core/tools/runSlashCommandTool.ts

@@ -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
+	}
+}

+ 6 - 0
src/shared/ExtensionMessage.ts

@@ -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.

+ 3 - 0
src/shared/__tests__/experiments.spec.ts

@@ -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)
 		})

+ 2 - 0
src/shared/experiments.ts

@@ -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(

+ 7 - 0
src/shared/tools.ts

@@ -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 =

+ 143 - 0
webview-ui/src/components/chat/ChatRow.tsx

@@ -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 || "{}")

+ 1 - 0
webview-ui/src/components/chat/ChatView.tsx

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

+ 127 - 0
webview-ui/src/components/chat/__tests__/ChatRow.run-slash-command.spec.tsx

@@ -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()
+	})
+})

+ 2 - 0
webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx

@@ -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,
 		})
 	})
 })

+ 4 - 0
webview-ui/src/i18n/locales/ca/chat.json

@@ -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:"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/ca/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/de/chat.json

@@ -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:"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/de/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/en/chat.json

@@ -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"

+ 4 - 0
webview-ui/src/i18n/locales/en/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/es/chat.json

@@ -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:"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/es/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/fr/chat.json

@@ -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:"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/fr/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/hi/chat.json

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

+ 4 - 0
webview-ui/src/i18n/locales/hi/settings.json

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

+ 4 - 0
webview-ui/src/i18n/locales/id/chat.json

@@ -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:"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/id/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/it/chat.json

@@ -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:"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/it/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/ja/chat.json

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

+ 4 - 0
webview-ui/src/i18n/locales/ja/settings.json

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

+ 4 - 0
webview-ui/src/i18n/locales/ko/chat.json

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

+ 4 - 0
webview-ui/src/i18n/locales/ko/settings.json

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

+ 4 - 0
webview-ui/src/i18n/locales/nl/chat.json

@@ -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:"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/nl/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/pl/chat.json

@@ -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:"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/pl/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/pt-BR/chat.json

@@ -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:"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/ru/chat.json

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

+ 4 - 0
webview-ui/src/i18n/locales/ru/settings.json

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

+ 4 - 0
webview-ui/src/i18n/locales/tr/chat.json

@@ -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ı:"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/tr/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/vi/chat.json

@@ -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:"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/vi/settings.json

@@ -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": {

+ 4 - 0
webview-ui/src/i18n/locales/zh-CN/chat.json

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

+ 4 - 0
webview-ui/src/i18n/locales/zh-CN/settings.json

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

+ 4 - 0
webview-ui/src/i18n/locales/zh-TW/chat.json

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

+ 4 - 0
webview-ui/src/i18n/locales/zh-TW/settings.json

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