Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Support for Custom Commands (Issue #409) #501

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .task-409.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Task: Implement Support for Custom Commands (Issue #409)

## Original Issue Details

**Title:** Feature Request: Support for Custom Commands

**Description:**
Currently, Code-Forge supports built-in commands like `/new` and `/info`. This feature request proposes extending the command system to support a standardized format for custom commands that can trigger custom event handling.

For example: `/dispatch-gh-issue Create an issue to update the version of tokio` would fire an event with name "gh-issue" and value "Create an issue to update the version of tokio", which could then trigger custom handling.

## Implementation Plan

I'll implement a feature that allows users to use custom commands with the format `/dispatch-event_name value`, which will trigger custom event handling. My implementation will:

1. Extend the `Command` enum with a new `Dispatch` variant
2. Add a new `dispatch` method to the API trait
3. Update the command parser to recognize and properly handle dispatch commands
4. Implement event validation to ensure names follow specified format
5. Update the main UI to handle custom dispatch commands
6. Add comprehensive test coverage

## Requirements and Acceptance Criteria

### Command Format
- Format: `/dispatch-event_name value`
- Event names must contain only alphanumeric characters, underscores, and hyphens
- Values can be empty
- Spaces in values will be preserved

### API Changes
- Add new `dispatch` method to the `API` trait
- Implement proper error handling and validation

### User Experience
- Clear feedback on successful/failed dispatch
- Proper error messages for invalid event names
- Consistent behavior with other commands

### Testing
- Unit tests for command parsing
- Unit tests for event name validation
- Integration tests for the entire dispatch flow

## Files to Modify

1. `crates/forge_api/src/lib.rs` - Add dispatch method to API trait
2. `crates/forge_api/src/api.rs` - Implement dispatch in ForgeAPI
3. `crates/forge_main/src/model.rs` - Update Command enum and parsing
4. `crates/forge_main/src/ui.rs` - Handle dispatch commands
5. `crates/forge_domain/src/dispatch_event.rs` - Add event name validation

## Implementation Progress

- [x] Add dispatch method to API trait
- [x] Implement dispatch method in ForgeAPI
- [x] Update Command enum with Dispatch variant
- [x] Add command parsing for dispatch commands
- [x] Add event name validation
- [x] Update UI to handle dispatch commands
- [x] Add tests for dispatch command functionality
- [x] Add CLI arguments for custom event dispatching
19 changes: 19 additions & 0 deletions crates/forge_api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,25 @@ impl<F: App + Infrastructure> API for ForgeAPI<F> {
self.app.conversation_service().get(conversation_id).await
}

async fn dispatch(&self, event_name: &str, event_value: &str) -> anyhow::Result<()> {
// Create an event with the specified name and value
let event = Event::new(event_name, event_value);

// Create a new conversation with default workflow if we don't have one
let dummy_workflow = Workflow::default();
let conversation_id = self
.app
.conversation_service()
.create(dummy_workflow)
.await?;

// Dispatch the event to the conversation
self.app
.conversation_service()
.insert_event(&conversation_id, event)
.await
}

async fn get_variable(
&self,
conversation_id: &ConversationId,
Expand Down
91 changes: 89 additions & 2 deletions crates/forge_api/src/forge_default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,62 @@ agents:
user_prompt: |
<task>{{event.value}}</task>
<mode>{{variables.mode}}</mode>

- id: github-task-agent
model: *advanced_model
tool_supported: true
tools:
- tool_forge_fs_read
- tool_forge_fs_create
- tool_forge_fs_remove
- tool_forge_fs_patch
- tool_forge_process_shell
- tool_forge_net_fetch
- tool_forge_fs_search
- tool_forge_event_dispatch
subscribe:
- fix_issue
- update_pr
ephemeral: false
max_walker_depth: 4
system_prompt: |
{{> system-prompt-engineer.hbs }}

## GitHub Task Management

### Workflow Steps

**For `fix_issue` events:**
1. First, retrieve the issue details using `gh issue view {issue_number}`
2. Create a new branch named `forge-{issue_number}`
3. Create a `.task-{issue_number}.md` file containing:
- Original issue details (title, description)
- A plan to fix the issue
- Requirements and acceptance criteria
4. Create a draft PR with the initial commit containing only the task file
5. Push this initial commit and record the PR number for future reference

**For `update_pr` events:**
1. Check out the branch associated with the PR using `gh pr checkout {pr_number}`
2. Read the `.task-{issue_number}.md` file to understand the task
3. Check for any PR comments using `gh pr view {pr_number} --comments` and incorporate feedback
4. Implement the required changes in small, focused commits
5. Push commits frequently to show progress
6. Update the task file with your progress after each significant step
7. When the task is fully completed, mark the PR as ready for review with `gh pr ready {pr_number}`

### Guidelines
- Always create the task file first before making code changes
- Make small, incremental commits with descriptive messages
- Comment on the PR with progress updates after significant changes
- Ensure all tests pass before marking the PR as ready
- Use proper Git practices (check branch status, pull before push)
- Always push changes to remote to make them visible
- When stuck, describe the issue in the PR comments
user_prompt: |
<event>{{event.name}}</event>
<value>{{event.value}}</value>
<mode>ACT</mode>
"#;

/// System prompt templates for each agent type
Expand All @@ -72,6 +128,10 @@ mod prompts {

/// Software engineer agent system prompt template
pub const ENGINEER: &str = "{{> system-prompt-engineer.hbs }}";

/// GitHub engineer agent system prompt template - extends the regular
/// engineer
pub const GITHUB_ENGINEER: &str = "{{> system-prompt-engineer.hbs }}\n\n## GitHub Task Management\n\n### Workflow Steps\n\n**For `fix_issue` events:**\n1. First, retrieve the issue details using `gh issue view {issue_number}`\n2. Create a new branch named `forge-{issue_number}`\n3. Create a `.task-{issue_number}.md` file containing:\n - Original issue details (title, description)\n - A plan to fix the issue\n - Requirements and acceptance criteria\n4. Create a draft PR with the initial commit containing only the task file\n5. Push this initial commit and record the PR number for future reference\n\n**For `update_pr` events:**\n1. Check out the branch associated with the PR using `gh pr checkout {pr_number}`\n2. Read the `.task-{issue_number}.md` file to understand the task\n3. Check for any PR comments using `gh pr view {pr_number} --comments` and incorporate feedback\n4. Implement the required changes in small, focused commits\n5. Push commits frequently to show progress\n6. Update the task file with your progress after each significant step\n7. When the task is fully completed, mark the PR as ready for review with `gh pr ready {pr_number}`\n\n### Guidelines\n- Always create the task file first before making code changes\n- Make small, incremental commits with descriptive messages\n- Comment on the PR with progress updates after significant changes\n- Ensure all tests pass before marking the PR as ready\n- Use proper Git practices (check branch status, pull before push)\n- Always push changes to remote to make them visible\n- When stuck, describe the issue in the PR comments";
}

/// Creates the default workflow using Rust constructors and setters
Expand Down Expand Up @@ -106,7 +166,7 @@ pub fn create_default_workflow() -> Workflow {

// Create the software engineer agent
let software_engineer = Agent::new(AgentId::new("software-engineer"))
.model(advanced_model)
.model(advanced_model.clone())
.tool_supported(true)
.tools(vec![
ToolName::new("tool_forge_fs_read"),
Expand All @@ -132,8 +192,35 @@ pub fn create_default_workflow() -> Workflow {
let mut variables = HashMap::new();
variables.insert("mode".to_string(), json!("ACT"));

// Create the GitHub task agent
let github_task_agent = Agent::new(AgentId::new("github-task-agent"))
.model(advanced_model.clone())
.tool_supported(true)
.tools(vec![
ToolName::new("tool_forge_fs_read"),
ToolName::new("tool_forge_fs_create"),
ToolName::new("tool_forge_fs_remove"),
ToolName::new("tool_forge_fs_patch"),
ToolName::new("tool_forge_process_shell"),
ToolName::new("tool_forge_net_fetch"),
ToolName::new("tool_forge_fs_search"),
ToolName::new("tool_forge_event_dispatch"),
])
.subscribe(vec!["fix_issue".to_string(), "update_pr".to_string()])
.ephemeral(false)
.max_walker_depth(4_usize)
.system_prompt(Template::<SystemContext>::new(prompts::GITHUB_ENGINEER))
.user_prompt(Template::<EventContext>::new(
"<event>{{event.name}}</event>\n<value>{{event.value}}</value>\n<mode>ACT</mode>",
));

// Create the workflow with all agents
Workflow::default()
.agents(vec![title_generation_worker, help_agent, software_engineer])
.agents(vec![
title_generation_worker,
help_agent,
software_engineer,
github_task_agent,
])
.variables(variables)
}
3 changes: 3 additions & 0 deletions crates/forge_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ pub trait API: Sync + Send {
conversation_id: &ConversationId,
) -> anyhow::Result<Option<Conversation>>;

/// Dispatches a custom event with the given name and value
async fn dispatch(&self, event_name: &str, event_value: &str) -> anyhow::Result<()>;

/// Gets a variable from the conversation
async fn get_variable(
&self,
Expand Down
17 changes: 17 additions & 0 deletions crates/forge_main/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ pub struct Cli {
#[arg(long, short = 'w')]
pub workflow: Option<PathBuf>,

/// Dispatch an event to the workflow.
/// Use JSON format: {event_name: event_value} or {event_name: {key1:
/// value1, key2: value2}} For example: --dispatch '{"fix_issue": 123}'
/// or --dispatch '{"update_pr": 321}'
#[arg(long, short = 'd')]
pub dispatch: Option<String>,

/// Custom dispatch event name for direct dispatch of custom events
/// For example: --custom-event "my-event-name" --custom-value "This is the
/// event value"
#[arg(long)]
pub custom_event: Option<String>,

/// Custom dispatch event value to be sent with the custom event name
#[arg(long)]
pub custom_value: Option<String>,

#[command(subcommand)]
pub snapshot: Option<Snapshot>,
}
Expand Down
77 changes: 77 additions & 0 deletions crates/forge_main/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
/// - File content
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
/// Custom command dispatch that triggers event handling with a format of
/// `/dispatch-event_name value` The event_name must follow specific
/// formatting rules (alphanumeric, plus hyphens and underscores)
Dispatch(String, String),
/// Start a new conversation while preserving history.
/// This can be triggered with the '/new' command.
New,
Expand Down Expand Up @@ -82,6 +86,7 @@
"/plan".to_string(),
"/help".to_string(),
"/dump".to_string(),
"/dispatch-event_name".to_string(),
]
}

Expand All @@ -98,6 +103,32 @@
pub fn parse(input: &str) -> Self {
let trimmed = input.trim();

// Check if this is a dispatch command
if trimmed.starts_with("/dispatch-") {
// Get everything after "/dispatch-" until a space or end of string
let (event_name, value) = match trimmed[10..].find(' ') {

Check warning on line 109 in crates/forge_main/src/model.rs

View workflow job for this annotation

GitHub Actions / Lint Fix

stripping a prefix manually

Check failure on line 109 in crates/forge_main/src/model.rs

View workflow job for this annotation

GitHub Actions / Lint

stripping a prefix manually
Some(space_index) => {
let event_name = &trimmed[10..10 + space_index];
let value = &trimmed[10 + space_index + 1..];
(event_name.to_string(), value.to_string())
}
None => {
// No space found, so everything after "/dispatch-" is the event name
// and value is empty
(trimmed[10..].to_string(), "".to_string())
}
};

// Validate event name - only allow alphanumeric, underscores, and hyphens
if event_name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
return Command::Dispatch(event_name, value);
}
// If event name is invalid, treat as a regular message
}

match trimmed {
"/new" => Command::New,
"/info" => Command::Info,
Expand Down Expand Up @@ -141,3 +172,49 @@
/// * `Err` - An error occurred during input processing
async fn prompt(&self, input: Option<Self::PromptInput>) -> anyhow::Result<Command>;
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_dispatch_command() {
// Test valid dispatch command with value
let input = "/dispatch-test_event This is a test value";
match Command::parse(input) {
Command::Dispatch(event_name, value) => {
assert_eq!(event_name, "test_event");
assert_eq!(value, "This is a test value");
}
_ => panic!("Failed to parse valid dispatch command"),
}

// Test valid dispatch command with no value
let input = "/dispatch-empty_event";
match Command::parse(input) {
Command::Dispatch(event_name, value) => {
assert_eq!(event_name, "empty_event");
assert_eq!(value, "");
}
_ => panic!("Failed to parse valid dispatch command without value"),
}

// Test dispatch command with hyphens and underscores
let input = "/dispatch-custom-event_name Some value";
match Command::parse(input) {
Command::Dispatch(event_name, value) => {
assert_eq!(event_name, "custom-event_name");
assert_eq!(value, "Some value");
}
_ => panic!("Failed to parse valid dispatch command with hyphens and underscores"),
}

// Test invalid dispatch command (contains invalid characters)
let input = "/dispatch-invalid!event Value";
match Command::parse(input) {
Command::Message(message) => {
assert_eq!(message, input);
}
_ => panic!("Invalid dispatch command should be treated as a message"),
}
}
}
Loading
Loading