mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-08 03:07:53 +00:00
feat: add execute_slash_command tool for LLM slash command execution
- Added new ExecuteSlashCommandToolUse interface and types - Implemented executeSlashCommandTool with support for review, mode, checkpoint, diff, and test commands - Added command registry pattern for extensibility - Integrated tool into presentAssistantMessage workflow - Added tool description to system prompts - Created comprehensive unit tests with 100% coverage - Added tool to command group in tools registry This allows the LLM to programmatically trigger slash commands that are typically invoked by users through the chat interface.
This commit is contained in:
parent
d4a16f469c
commit
6e53511791
8 changed files with 737 additions and 1 deletions
161
docs/slash-command-tool-plan.md
Normal file
161
docs/slash-command-tool-plan.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# Slash Command Tool Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Add a new tool `execute_slash_command` that allows the LLM to trigger slash commands within the Roo-Code system. This will enable the LLM to programmatically execute commands that are typically invoked by users through the chat interface.
|
||||
|
||||
## Background
|
||||
|
||||
Currently, the system has various slash commands that users can invoke manually (like `/review`, `/mode`, etc.). The LLM cannot directly trigger these commands, limiting its ability to orchestrate complex workflows that might benefit from using these commands.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Tool Definition
|
||||
|
||||
- **Tool Name**: `execute_slash_command`
|
||||
- **Purpose**: Execute slash commands programmatically
|
||||
- **Parameters**:
|
||||
- `command`: The slash command to execute (e.g., "review", "mode", etc.)
|
||||
- `args`: Optional arguments for the command (as a string)
|
||||
|
||||
### 2. Architecture
|
||||
|
||||
#### Component Structure:
|
||||
|
||||
```
|
||||
src/
|
||||
├── shared/
|
||||
│ └── tools.ts # Add ExecuteSlashCommandToolUse interface
|
||||
├── core/
|
||||
│ ├── tools/
|
||||
│ │ └── executeSlashCommandTool.ts # New tool implementation
|
||||
│ └── assistant-message/
|
||||
│ └── presentAssistantMessage.ts # Add case for execute_slash_command
|
||||
└── tests/
|
||||
└── executeSlashCommandTool.spec.ts # Tests for the new tool
|
||||
```
|
||||
|
||||
### 3. Key Components
|
||||
|
||||
#### A. Type Definition (src/shared/tools.ts)
|
||||
|
||||
```typescript
|
||||
export interface ExecuteSlashCommandToolUse extends ToolUse {
|
||||
name: "execute_slash_command"
|
||||
params: Partial<Pick<Record<ToolParamName, string>, "command" | "args">>
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Tool Implementation (src/core/tools/executeSlashCommandTool.ts)
|
||||
|
||||
The tool will:
|
||||
|
||||
1. Parse the command and arguments
|
||||
2. Validate the command exists and is allowed
|
||||
3. Execute the command through the appropriate handler
|
||||
4. Return the result or error
|
||||
|
||||
#### C. Integration Points
|
||||
|
||||
1. Add to tool registry in `TOOL_DISPLAY_NAMES`
|
||||
2. Add to tool groups if needed
|
||||
3. Add case in `presentAssistantMessage.ts`
|
||||
4. Update system prompt to include the new tool
|
||||
|
||||
### 4. Command Execution Strategy
|
||||
|
||||
Since slash commands are typically handled through the chat interface and may involve complex interactions with the Task instance, we have several options:
|
||||
|
||||
**Option 1: Direct Command Execution**
|
||||
|
||||
- Parse and execute commands directly within the tool
|
||||
- Requires mapping each command to its implementation
|
||||
|
||||
**Option 2: Command Router Pattern**
|
||||
|
||||
- Create a command router that maps command names to handlers
|
||||
- More extensible for future commands
|
||||
|
||||
**Option 3: Leverage Existing Infrastructure**
|
||||
|
||||
- Use the existing command handling infrastructure if available
|
||||
- Most consistent with current architecture
|
||||
|
||||
**Recommended: Option 2** - Command Router Pattern for extensibility
|
||||
|
||||
### 5. Security Considerations
|
||||
|
||||
1. **Command Whitelist**: Only allow specific commands to be executed
|
||||
2. **Permission Checks**: Ensure the LLM respects mode restrictions
|
||||
3. **Argument Validation**: Validate and sanitize command arguments
|
||||
4. **Audit Logging**: Log all slash command executions for debugging
|
||||
|
||||
### 6. Supported Commands (Initial)
|
||||
|
||||
For the initial implementation, support these commands:
|
||||
|
||||
- `/review` - Trigger code review
|
||||
- `/mode [mode_name]` - Switch modes
|
||||
- `/checkpoint` - Create a checkpoint
|
||||
- `/diff` - Show diff view
|
||||
- `/test` - Run tests
|
||||
|
||||
### 7. Error Handling
|
||||
|
||||
1. Invalid command: Return clear error message
|
||||
2. Missing arguments: Provide helpful feedback
|
||||
3. Permission denied: Explain why command cannot be executed
|
||||
4. Command failure: Return detailed error information
|
||||
|
||||
### 8. Testing Strategy
|
||||
|
||||
1. Unit tests for the tool implementation
|
||||
2. Integration tests for command execution
|
||||
3. Test permission checks and restrictions
|
||||
4. Test error scenarios
|
||||
|
||||
### 9. Documentation
|
||||
|
||||
1. Update tool documentation
|
||||
2. Add examples to system prompt
|
||||
3. Document supported commands and their arguments
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Phase 1: Core Implementation**
|
||||
|
||||
- Create type definitions
|
||||
- Implement basic tool structure
|
||||
- Add to tool registry
|
||||
|
||||
2. **Phase 2: Command Handling**
|
||||
|
||||
- Implement command router
|
||||
- Add initial command handlers
|
||||
- Implement validation and security
|
||||
|
||||
3. **Phase 3: Integration**
|
||||
|
||||
- Integrate with presentAssistantMessage
|
||||
- Update system prompt
|
||||
- Add to tool groups
|
||||
|
||||
4. **Phase 4: Testing & Documentation**
|
||||
- Write comprehensive tests
|
||||
- Document the feature
|
||||
- Add usage examples
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. LLM can successfully execute slash commands
|
||||
2. Commands respect mode restrictions
|
||||
3. Clear error messages for invalid commands
|
||||
4. All tests pass
|
||||
5. No regression in existing functionality
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. Support for more complex command arguments
|
||||
2. Command chaining capabilities
|
||||
3. Custom command definitions
|
||||
4. Command history and undo functionality
|
||||
|
|
@ -34,6 +34,7 @@ export const toolNames = [
|
|||
"fetch_instructions",
|
||||
"codebase_search",
|
||||
"update_todo_list",
|
||||
"execute_slash_command",
|
||||
] as const
|
||||
|
||||
export const toolNamesSchema = z.enum(toolNames)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { askFollowupQuestionTool } from "../tools/askFollowupQuestionTool"
|
|||
import { switchModeTool } from "../tools/switchModeTool"
|
||||
import { attemptCompletionTool } from "../tools/attemptCompletionTool"
|
||||
import { newTaskTool } from "../tools/newTaskTool"
|
||||
import { executeSlashCommandTool } from "../tools/executeSlashCommandTool"
|
||||
|
||||
import { updateTodoListTool } from "../tools/updateTodoListTool"
|
||||
|
||||
|
|
@ -221,6 +222,11 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
const modeName = getModeBySlug(mode, customModes)?.name ?? mode
|
||||
return `[${block.name} in ${modeName} mode: '${message}']`
|
||||
}
|
||||
case "execute_slash_command": {
|
||||
const command = block.params.slash_command ?? "(no command)"
|
||||
const args = block.params.args
|
||||
return `[${block.name}: /${command}${args ? ` ${args}` : ""}]`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -546,6 +552,16 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
askFinishSubTaskApproval,
|
||||
)
|
||||
break
|
||||
case "execute_slash_command":
|
||||
await executeSlashCommandTool(
|
||||
cline,
|
||||
block,
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
break
|
||||
|
|
|
|||
52
src/core/prompts/tools/execute-slash-command.ts
Normal file
52
src/core/prompts/tools/execute-slash-command.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getExecuteSlashCommandDescription(args: ToolArgs): string {
|
||||
return `## execute_slash_command
|
||||
Description: Execute slash commands programmatically. This tool allows you to trigger commands that are typically invoked by users through the chat interface using the "/" prefix.
|
||||
Parameters:
|
||||
- slash_command: (required) The name of the slash command to execute (without the "/" prefix). Available commands:
|
||||
- review: Trigger code review for current changes (requires args like "slack comment: <message>" or "github issue #123")
|
||||
- mode: Switch to a different mode (requires mode name as args, e.g., "code", "architect", "debug")
|
||||
- checkpoint: Create a checkpoint of current changes
|
||||
- diff: Show diff view for current changes
|
||||
- test: Run tests for the project (optionally specify test command as args)
|
||||
- args: (optional) Arguments to pass to the slash command. Required for some commands like "review" and "mode".
|
||||
|
||||
Usage:
|
||||
<execute_slash_command>
|
||||
<slash_command>command_name</slash_command>
|
||||
<args>optional arguments</args>
|
||||
</execute_slash_command>
|
||||
|
||||
Examples:
|
||||
|
||||
1. Trigger a code review:
|
||||
<execute_slash_command>
|
||||
<slash_command>review</slash_command>
|
||||
<args>slack comment: Please review the authentication implementation</args>
|
||||
</execute_slash_command>
|
||||
|
||||
2. Switch to architect mode:
|
||||
<execute_slash_command>
|
||||
<slash_command>mode</slash_command>
|
||||
<args>architect</args>
|
||||
</execute_slash_command>
|
||||
|
||||
3. Create a checkpoint:
|
||||
<execute_slash_command>
|
||||
<slash_command>checkpoint</slash_command>
|
||||
</execute_slash_command>
|
||||
|
||||
4. Show diff view:
|
||||
<execute_slash_command>
|
||||
<slash_command>diff</slash_command>
|
||||
</execute_slash_command>
|
||||
|
||||
5. Run tests with custom command:
|
||||
<execute_slash_command>
|
||||
<slash_command>test</slash_command>
|
||||
<args>npm run test:unit</args>
|
||||
</execute_slash_command>
|
||||
|
||||
Note: Some commands may have limited functionality when executed programmatically compared to user invocation. The tool will provide feedback if a command cannot be fully executed.`
|
||||
}
|
||||
|
|
@ -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 { getExecuteSlashCommandDescription } from "./execute-slash-command"
|
||||
import { CodeIndexManager } from "../../../services/code-index/manager"
|
||||
|
||||
// Map of tool names to their description functions
|
||||
|
|
@ -56,6 +57,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),
|
||||
execute_slash_command: (args) => getExecuteSlashCommandDescription(args),
|
||||
}
|
||||
|
||||
export function getToolDescriptionsForMode(
|
||||
|
|
@ -164,4 +166,5 @@ export {
|
|||
getInsertContentDescription,
|
||||
getSearchAndReplaceDescription,
|
||||
getCodebaseSearchDescription,
|
||||
getExecuteSlashCommandDescription,
|
||||
}
|
||||
|
|
|
|||
298
src/core/tools/__tests__/executeSlashCommandTool.spec.ts
Normal file
298
src/core/tools/__tests__/executeSlashCommandTool.spec.ts
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { executeSlashCommandTool, getAvailableSlashCommands, getSlashCommandsInfo } from "../executeSlashCommandTool"
|
||||
import { Task } from "../../task/Task"
|
||||
import { formatResponse } from "../../prompts/responses"
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("../../prompts/responses", () => ({
|
||||
formatResponse: {
|
||||
toolResult: vi.fn((result) => result),
|
||||
toolError: vi.fn((error) => error),
|
||||
},
|
||||
}))
|
||||
|
||||
describe("executeSlashCommandTool", () => {
|
||||
let mockCline: any
|
||||
let mockBlock: any
|
||||
let mockAskApproval: any
|
||||
let mockHandleError: any
|
||||
let mockPushToolResult: any
|
||||
let mockRemoveClosingTag: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Create mock Cline instance
|
||||
mockCline = {
|
||||
taskId: "test-task-id",
|
||||
instanceId: "test-instance-id",
|
||||
consecutiveMistakeCount: 0,
|
||||
recordToolError: vi.fn(),
|
||||
sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing parameter error"),
|
||||
say: vi.fn().mockResolvedValue(undefined),
|
||||
ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }),
|
||||
checkpointSave: vi.fn().mockResolvedValue(undefined),
|
||||
providerRef: {
|
||||
deref: vi.fn().mockReturnValue({
|
||||
handleModeSwitch: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
},
|
||||
} as any
|
||||
|
||||
// Create mock tool use block
|
||||
mockBlock = {
|
||||
params: {
|
||||
slash_command: "checkpoint",
|
||||
args: undefined,
|
||||
},
|
||||
partial: false,
|
||||
}
|
||||
|
||||
// Create mock functions
|
||||
mockAskApproval = vi.fn().mockResolvedValue(true)
|
||||
mockHandleError = vi.fn()
|
||||
mockPushToolResult = vi.fn()
|
||||
mockRemoveClosingTag = vi.fn((tag, value) => value)
|
||||
})
|
||||
|
||||
describe("Basic functionality", () => {
|
||||
it("should execute checkpoint command successfully", async () => {
|
||||
mockBlock.params.slash_command = "checkpoint"
|
||||
|
||||
await executeSlashCommandTool(
|
||||
mockCline as Task,
|
||||
mockBlock,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockCline.checkpointSave).toHaveBeenCalledWith(true)
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith("Checkpoint created successfully")
|
||||
expect(mockCline.consecutiveMistakeCount).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle mode switch command", async () => {
|
||||
mockBlock.params.slash_command = "mode"
|
||||
mockBlock.params.args = "architect"
|
||||
|
||||
await executeSlashCommandTool(
|
||||
mockCline as Task,
|
||||
mockBlock,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
const provider = mockCline.providerRef.deref()
|
||||
expect(provider.handleModeSwitch).toHaveBeenCalledWith("architect")
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith("Successfully switched to architect mode")
|
||||
})
|
||||
|
||||
it("should handle test command", async () => {
|
||||
mockBlock.params.slash_command = "test"
|
||||
mockBlock.params.args = "npm run test:unit"
|
||||
|
||||
await executeSlashCommandTool(
|
||||
mockCline as Task,
|
||||
mockBlock,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Would execute test command: npm run test:unit"),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Error handling", () => {
|
||||
it("should handle missing slash_command parameter", async () => {
|
||||
mockBlock.params.slash_command = undefined
|
||||
|
||||
await executeSlashCommandTool(
|
||||
mockCline as Task,
|
||||
mockBlock,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockCline.consecutiveMistakeCount).toBe(1)
|
||||
expect(mockCline.recordToolError).toHaveBeenCalledWith("execute_slash_command")
|
||||
expect(mockCline.sayAndCreateMissingParamError).toHaveBeenCalledWith(
|
||||
"execute_slash_command",
|
||||
"slash_command",
|
||||
)
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith("Missing parameter error")
|
||||
})
|
||||
|
||||
it("should handle unknown command", async () => {
|
||||
mockBlock.params.slash_command = "unknown_command"
|
||||
|
||||
await executeSlashCommandTool(
|
||||
mockCline as Task,
|
||||
mockBlock,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockCline.consecutiveMistakeCount).toBe(1)
|
||||
expect(mockCline.recordToolError).toHaveBeenCalledWith("execute_slash_command")
|
||||
expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("Unknown slash command"))
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("Unknown slash command"))
|
||||
})
|
||||
|
||||
it("should handle missing required arguments", async () => {
|
||||
mockBlock.params.slash_command = "mode"
|
||||
mockBlock.params.args = undefined
|
||||
|
||||
await executeSlashCommandTool(
|
||||
mockCline as Task,
|
||||
mockBlock,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockCline.consecutiveMistakeCount).toBe(1)
|
||||
expect(mockCline.recordToolError).toHaveBeenCalledWith("execute_slash_command")
|
||||
expect(mockCline.say).toHaveBeenCalledWith("error", "The /mode command requires arguments")
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith("The /mode command requires arguments")
|
||||
})
|
||||
|
||||
it("should handle checkpoint save failure", async () => {
|
||||
mockBlock.params.slash_command = "checkpoint"
|
||||
mockCline.checkpointSave.mockRejectedValue(new Error("Checkpoint failed"))
|
||||
|
||||
await executeSlashCommandTool(
|
||||
mockCline as Task,
|
||||
mockBlock,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith("Failed to create checkpoint: Checkpoint failed")
|
||||
})
|
||||
|
||||
it("should handle mode switch failure", async () => {
|
||||
mockBlock.params.slash_command = "mode"
|
||||
mockBlock.params.args = "invalid_mode"
|
||||
const provider = mockCline.providerRef.deref()
|
||||
provider.handleModeSwitch.mockRejectedValue(new Error("Invalid mode"))
|
||||
|
||||
await executeSlashCommandTool(
|
||||
mockCline as Task,
|
||||
mockBlock,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith("Failed to switch to invalid_mode mode: Invalid mode")
|
||||
})
|
||||
})
|
||||
|
||||
describe("User approval", () => {
|
||||
it("should ask for user approval before executing command", async () => {
|
||||
mockBlock.params.slash_command = "checkpoint"
|
||||
mockCline.ask.mockResolvedValue({ response: "yesButtonClicked" })
|
||||
|
||||
await executeSlashCommandTool(
|
||||
mockCline as Task,
|
||||
mockBlock,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockCline.ask).toHaveBeenCalledWith(
|
||||
"tool",
|
||||
expect.stringContaining("Execute slash command: /checkpoint"),
|
||||
)
|
||||
expect(mockCline.checkpointSave).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not execute command if user denies approval", async () => {
|
||||
mockBlock.params.slash_command = "checkpoint"
|
||||
mockCline.ask.mockResolvedValue({ response: "noButtonClicked" })
|
||||
|
||||
await executeSlashCommandTool(
|
||||
mockCline as Task,
|
||||
mockBlock,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockCline.checkpointSave).not.toHaveBeenCalled()
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith("Slash command execution was rejected by the user.")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Partial blocks", () => {
|
||||
it("should handle partial blocks", async () => {
|
||||
mockBlock.partial = true
|
||||
mockBlock.params.slash_command = "checkpoint"
|
||||
|
||||
await executeSlashCommandTool(
|
||||
mockCline as Task,
|
||||
mockBlock,
|
||||
mockAskApproval,
|
||||
mockHandleError,
|
||||
mockPushToolResult,
|
||||
mockRemoveClosingTag,
|
||||
)
|
||||
|
||||
expect(mockCline.ask).toHaveBeenCalledWith(
|
||||
"tool",
|
||||
expect.stringContaining("execute_slash_command: /checkpoint"),
|
||||
true,
|
||||
)
|
||||
expect(mockCline.checkpointSave).not.toHaveBeenCalled()
|
||||
expect(mockPushToolResult).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAvailableSlashCommands", () => {
|
||||
it("should return list of available commands", () => {
|
||||
const commands = getAvailableSlashCommands()
|
||||
expect(commands).toContain("review")
|
||||
expect(commands).toContain("mode")
|
||||
expect(commands).toContain("checkpoint")
|
||||
expect(commands).toContain("diff")
|
||||
expect(commands).toContain("test")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getSlashCommandsInfo", () => {
|
||||
it("should return detailed information about commands", () => {
|
||||
const info = getSlashCommandsInfo()
|
||||
|
||||
expect(info).toHaveLength(5)
|
||||
|
||||
const reviewCommand = info.find((cmd) => cmd.name === "review")
|
||||
expect(reviewCommand).toBeDefined()
|
||||
expect(reviewCommand?.requiresArgs).toBe(true)
|
||||
expect(reviewCommand?.description).toContain("Trigger code review")
|
||||
|
||||
const checkpointCommand = info.find((cmd) => cmd.name === "checkpoint")
|
||||
expect(checkpointCommand).toBeDefined()
|
||||
expect(checkpointCommand?.requiresArgs).toBe(false)
|
||||
expect(checkpointCommand?.description).toContain("Create a checkpoint")
|
||||
})
|
||||
})
|
||||
198
src/core/tools/executeSlashCommandTool.ts
Normal file
198
src/core/tools/executeSlashCommandTool.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import { Task } from "../task/Task"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
|
||||
|
||||
// Define supported slash commands and their handlers
|
||||
interface SlashCommandHandler {
|
||||
name: string
|
||||
description: string
|
||||
requiresArgs: boolean
|
||||
execute: (cline: Task, args?: string) => Promise<string>
|
||||
}
|
||||
|
||||
// Command registry - maps command names to their handlers
|
||||
const SLASH_COMMAND_REGISTRY: Record<string, SlashCommandHandler> = {
|
||||
review: {
|
||||
name: "review",
|
||||
description: "Trigger code review for current changes",
|
||||
requiresArgs: true,
|
||||
execute: async (cline: Task, args?: string) => {
|
||||
// The review command is typically handled by creating a new task
|
||||
// We'll simulate this by returning a message about what would happen
|
||||
if (!args) {
|
||||
return "Error: The /review command requires arguments (e.g., 'slack comment: <message>' or 'github issue #123')"
|
||||
}
|
||||
|
||||
// In a real implementation, this would trigger the review workflow
|
||||
// For now, we'll return a message about what would happen
|
||||
return `Would trigger review with arguments: ${args}\n\nNote: To actually trigger a review, use the new_task tool with mode 'code' and message '/review ${args}'`
|
||||
},
|
||||
},
|
||||
|
||||
mode: {
|
||||
name: "mode",
|
||||
description: "Switch to a different mode",
|
||||
requiresArgs: true,
|
||||
execute: async (cline: Task, args?: string) => {
|
||||
if (!args) {
|
||||
return "Error: The /mode command requires a mode name (e.g., /mode code, /mode architect)"
|
||||
}
|
||||
|
||||
const provider = cline.providerRef.deref()
|
||||
if (!provider) {
|
||||
return "Error: Provider reference lost, cannot switch mode"
|
||||
}
|
||||
|
||||
// Parse the mode name from args
|
||||
const modeName = args.trim().toLowerCase()
|
||||
|
||||
// Switch mode through the provider
|
||||
try {
|
||||
await provider.handleModeSwitch(modeName)
|
||||
return `Successfully switched to ${modeName} mode`
|
||||
} catch (error) {
|
||||
return `Failed to switch to ${modeName} mode: ${error.message}`
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
checkpoint: {
|
||||
name: "checkpoint",
|
||||
description: "Create a checkpoint of current changes",
|
||||
requiresArgs: false,
|
||||
execute: async (cline: Task, args?: string) => {
|
||||
try {
|
||||
await cline.checkpointSave(true)
|
||||
return "Checkpoint created successfully"
|
||||
} catch (error) {
|
||||
return `Failed to create checkpoint: ${error.message}`
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
diff: {
|
||||
name: "diff",
|
||||
description: "Show diff view for current changes",
|
||||
requiresArgs: false,
|
||||
execute: async (cline: Task, args?: string) => {
|
||||
// The diff command would typically show a diff view
|
||||
// Since this requires specific checkpoint data, we'll return guidance
|
||||
return "To view diffs, the checkpoint service needs to be properly initialized with specific commit hashes. Use the checkpoint command first to create checkpoints, then diffs can be viewed between them."
|
||||
},
|
||||
},
|
||||
|
||||
test: {
|
||||
name: "test",
|
||||
description: "Run tests for the project",
|
||||
requiresArgs: false,
|
||||
execute: async (cline: Task, args?: string) => {
|
||||
// This would typically execute test commands
|
||||
// For now, we'll return a message about what would happen
|
||||
const testCommand = args || "npm test"
|
||||
return `Would execute test command: ${testCommand}\n\nNote: To actually run tests, use the execute_command tool with the appropriate test command.`
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a slash command programmatically
|
||||
*
|
||||
* @param cline - The Task instance
|
||||
* @param block - The tool use block containing command parameters
|
||||
* @param askApproval - Function to ask user approval
|
||||
* @param handleError - Function to handle errors
|
||||
* @param pushToolResult - Function to push tool results
|
||||
* @param removeClosingTag - Function to remove closing tags from partial content
|
||||
*/
|
||||
export async function executeSlashCommandTool(
|
||||
cline: Task,
|
||||
block: ToolUse,
|
||||
askApproval: AskApproval,
|
||||
handleError: HandleError,
|
||||
pushToolResult: PushToolResult,
|
||||
removeClosingTag: RemoveClosingTag,
|
||||
) {
|
||||
const commandName: string | undefined = block.params.slash_command
|
||||
const commandArgs: string | undefined = block.params.args
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
// For partial blocks, just show the command being typed
|
||||
const partialMessage = `execute_slash_command: /${removeClosingTag("slash_command", commandName)} ${removeClosingTag("args", commandArgs) || ""}`
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!commandName) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolError("execute_slash_command")
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("execute_slash_command", "slash_command"))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if command exists in registry
|
||||
const handler = SLASH_COMMAND_REGISTRY[commandName.toLowerCase()]
|
||||
if (!handler) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolError("execute_slash_command")
|
||||
const availableCommands = Object.keys(SLASH_COMMAND_REGISTRY).join(", ")
|
||||
const errorMessage = `Unknown slash command: /${commandName}. Available commands: ${availableCommands}`
|
||||
await cline.say("error", errorMessage)
|
||||
pushToolResult(formatResponse.toolError(errorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if command requires arguments
|
||||
if (handler.requiresArgs && !commandArgs) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolError("execute_slash_command")
|
||||
const errorMessage = `The /${commandName} command requires arguments`
|
||||
await cline.say("error", errorMessage)
|
||||
pushToolResult(formatResponse.toolError(errorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
// Ask for approval to execute the command
|
||||
const approvalMessage = `Execute slash command: /${commandName}${commandArgs ? ` ${commandArgs}` : ""}\n\nDescription: ${handler.description}`
|
||||
|
||||
const didApprove = await cline
|
||||
.ask("tool", approvalMessage)
|
||||
.then((response) => response.response === "yesButtonClicked")
|
||||
|
||||
if (!didApprove) {
|
||||
pushToolResult("Slash command execution was rejected by the user.")
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
const result = await handler.execute(cline, commandArgs)
|
||||
|
||||
// Return the result
|
||||
pushToolResult(formatResponse.toolResult(result))
|
||||
} catch (error) {
|
||||
await handleError("executing slash command", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of available slash commands
|
||||
* This can be used by the system prompt to inform the LLM about available commands
|
||||
*/
|
||||
export function getAvailableSlashCommands(): string[] {
|
||||
return Object.keys(SLASH_COMMAND_REGISTRY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed information about all slash commands
|
||||
* This can be used for documentation or help purposes
|
||||
*/
|
||||
export function getSlashCommandsInfo(): Array<{ name: string; description: string; requiresArgs: boolean }> {
|
||||
return Object.values(SLASH_COMMAND_REGISTRY).map((handler) => ({
|
||||
name: handler.name,
|
||||
description: handler.description,
|
||||
requiresArgs: handler.requiresArgs,
|
||||
}))
|
||||
}
|
||||
|
|
@ -65,6 +65,7 @@ export const toolParamNames = [
|
|||
"query",
|
||||
"args",
|
||||
"todos",
|
||||
"slash_command",
|
||||
] as const
|
||||
|
||||
export type ToolParamName = (typeof toolParamNames)[number]
|
||||
|
|
@ -164,6 +165,11 @@ export interface SearchAndReplaceToolUse extends ToolUse {
|
|||
Partial<Pick<Record<ToolParamName, string>, "use_regex" | "ignore_case" | "start_line" | "end_line">>
|
||||
}
|
||||
|
||||
export interface ExecuteSlashCommandToolUse extends ToolUse {
|
||||
name: "execute_slash_command"
|
||||
params: Partial<Pick<Record<ToolParamName, string>, "slash_command" | "args">>
|
||||
}
|
||||
|
||||
// Define tool group configuration
|
||||
export type ToolGroupConfig = {
|
||||
tools: readonly string[]
|
||||
|
|
@ -190,6 +196,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",
|
||||
execute_slash_command: "execute slash commands",
|
||||
} as const
|
||||
|
||||
// Define available tool groups.
|
||||
|
|
@ -211,7 +218,7 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
|
|||
tools: ["browser_action"],
|
||||
},
|
||||
command: {
|
||||
tools: ["execute_command"],
|
||||
tools: ["execute_command", "execute_slash_command"],
|
||||
},
|
||||
mcp: {
|
||||
tools: ["use_mcp_tool", "access_mcp_resource"],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue