Skip to content

Commit e02c719

Browse files
feat: implement custom dispatch command support (Issue #409)
1 parent 2d85f3a commit e02c719

File tree

6 files changed

+138
-8
lines changed

6 files changed

+138
-8
lines changed

.task-409.md

+8-7
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,11 @@ I'll implement a feature that allows users to use custom commands with the forma
5252

5353
## Implementation Progress
5454

55-
- [ ] Add dispatch method to API trait
56-
- [ ] Implement dispatch method in ForgeAPI
57-
- [ ] Update Command enum with Dispatch variant
58-
- [ ] Add command parsing for dispatch commands
59-
- [ ] Add event name validation
60-
- [ ] Update UI to handle dispatch commands
61-
- [ ] Add tests for dispatch command functionality
55+
- [x] Add dispatch method to API trait
56+
- [x] Implement dispatch method in ForgeAPI
57+
- [x] Update Command enum with Dispatch variant
58+
- [x] Add command parsing for dispatch commands
59+
- [x] Add event name validation
60+
- [x] Update UI to handle dispatch commands
61+
- [x] Add tests for dispatch command functionality
62+
- [x] Add CLI arguments for custom event dispatching

crates/forge_api/src/api.rs

+12
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,18 @@ impl<F: App + Infrastructure> API for ForgeAPI<F> {
137137
self.app.conversation_service().get(conversation_id).await
138138
}
139139

140+
async fn dispatch(&self, event_name: &str, event_value: &str) -> anyhow::Result<()> {
141+
// Create an event with the specified name and value
142+
let event = Event::new(event_name, event_value);
143+
144+
// Create a new conversation with default workflow if we don't have one
145+
let dummy_workflow = Workflow::default();
146+
let conversation_id = self.app.conversation_service().create(dummy_workflow).await?;
147+
148+
// Dispatch the event to the conversation
149+
self.app.conversation_service().insert_event(&conversation_id, event).await
150+
}
151+
140152
async fn get_variable(
141153
&self,
142154
conversation_id: &ConversationId,

crates/forge_api/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ pub trait API: Sync + Send {
8080
conversation_id: &ConversationId,
8181
) -> anyhow::Result<Option<Conversation>>;
8282

83+
/// Dispatches a custom event with the given name and value
84+
async fn dispatch(&self, event_name: &str, event_value: &str) -> anyhow::Result<()>;
85+
8386
/// Gets a variable from the conversation
8487
async fn get_variable(
8588
&self,

crates/forge_main/src/cli.rs

+9
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ pub struct Cli {
5252
#[arg(long, short = 'd')]
5353
pub dispatch: Option<String>,
5454

55+
/// Custom dispatch event name for direct dispatch of custom events
56+
/// For example: --custom-event "my-event-name" --custom-value "This is the event value"
57+
#[arg(long)]
58+
pub custom_event: Option<String>,
59+
60+
/// Custom dispatch event value to be sent with the custom event name
61+
#[arg(long)]
62+
pub custom_value: Option<String>,
63+
5564
#[command(subcommand)]
5665
pub snapshot: Option<Snapshot>,
5766
}

crates/forge_main/src/model.rs

+73
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ impl From<&[Model]> for Info {
3939
/// - File content
4040
#[derive(Debug, Clone, PartialEq, Eq)]
4141
pub enum Command {
42+
/// Custom command dispatch that triggers event handling with a format of `/dispatch-event_name value`
43+
/// The event_name must follow specific formatting rules (alphanumeric, plus hyphens and underscores)
44+
Dispatch(String, String),
4245
/// Start a new conversation while preserving history.
4346
/// This can be triggered with the '/new' command.
4447
New,
@@ -82,6 +85,7 @@ impl Command {
8285
"/plan".to_string(),
8386
"/help".to_string(),
8487
"/dump".to_string(),
88+
"/dispatch-event_name".to_string(),
8589
]
8690
}
8791

@@ -98,6 +102,29 @@ impl Command {
98102
pub fn parse(input: &str) -> Self {
99103
let trimmed = input.trim();
100104

105+
// Check if this is a dispatch command
106+
if trimmed.starts_with("/dispatch-") {
107+
// Get everything after "/dispatch-" until a space or end of string
108+
let (event_name, value) = match trimmed[10..].find(' ') {
109+
Some(space_index) => {
110+
let event_name = &trimmed[10..10 + space_index];
111+
let value = &trimmed[10 + space_index + 1..];
112+
(event_name.to_string(), value.to_string())
113+
}
114+
None => {
115+
// No space found, so everything after "/dispatch-" is the event name
116+
// and value is empty
117+
(trimmed[10..].to_string(), "".to_string())
118+
}
119+
};
120+
121+
// Validate event name - only allow alphanumeric, underscores, and hyphens
122+
if event_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
123+
return Command::Dispatch(event_name, value);
124+
}
125+
// If event name is invalid, treat as a regular message
126+
}
127+
101128
match trimmed {
102129
"/new" => Command::New,
103130
"/info" => Command::Info,
@@ -141,3 +168,49 @@ pub trait UserInput {
141168
/// * `Err` - An error occurred during input processing
142169
async fn prompt(&self, input: Option<Self::PromptInput>) -> anyhow::Result<Command>;
143170
}
171+
#[cfg(test)]
172+
mod tests {
173+
use super::*;
174+
175+
#[test]
176+
fn test_parse_dispatch_command() {
177+
// Test valid dispatch command with value
178+
let input = "/dispatch-test_event This is a test value";
179+
match Command::parse(input) {
180+
Command::Dispatch(event_name, value) => {
181+
assert_eq!(event_name, "test_event");
182+
assert_eq!(value, "This is a test value");
183+
}
184+
_ => panic!("Failed to parse valid dispatch command"),
185+
}
186+
187+
// Test valid dispatch command with no value
188+
let input = "/dispatch-empty_event";
189+
match Command::parse(input) {
190+
Command::Dispatch(event_name, value) => {
191+
assert_eq!(event_name, "empty_event");
192+
assert_eq!(value, "");
193+
}
194+
_ => panic!("Failed to parse valid dispatch command without value"),
195+
}
196+
197+
// Test dispatch command with hyphens and underscores
198+
let input = "/dispatch-custom-event_name Some value";
199+
match Command::parse(input) {
200+
Command::Dispatch(event_name, value) => {
201+
assert_eq!(event_name, "custom-event_name");
202+
assert_eq!(value, "Some value");
203+
}
204+
_ => panic!("Failed to parse valid dispatch command with hyphens and underscores"),
205+
}
206+
207+
// Test invalid dispatch command (contains invalid characters)
208+
let input = "/dispatch-invalid!event Value";
209+
match Command::parse(input) {
210+
Command::Message(message) => {
211+
assert_eq!(message, input);
212+
}
213+
_ => panic!("Invalid dispatch command should be treated as a message"),
214+
}
215+
}
216+
}

crates/forge_main/src/ui.rs

+33-1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ impl<F: API> UI<F> {
104104
return self.handle_dispatch(dispatch_json).await;
105105
}
106106

107+
// Check for custom event dispatch
108+
if let Some(event_name) = self.cli.custom_event.clone() {
109+
let event_value = self.cli.custom_value.clone().unwrap_or_default();
110+
return self.handle_dispatch_command(&event_name, &event_value).await;
111+
}
112+
107113
if let Some(snapshot_command) = self.cli.snapshot.as_ref() {
108114
return match snapshot_command {
109115
Snapshot::Snapshot { sub_command } => self.handle_snaps(sub_command).await,
@@ -187,7 +193,12 @@ impl<F: API> UI<F> {
187193
input = self.console.prompt(prompt_input).await?;
188194
continue;
189195
}
190-
Command::Exit => {
196+
Command::Dispatch(event_name, value) => {
197+
self.handle_dispatch_command(&event_name, &value).await?;
198+
let prompt_input = Some((&self.state).into());
199+
input = self.console.prompt(prompt_input).await?;
200+
continue;
201+
} Command::Exit => {
191202
break;
192203
}
193204
Command::Models => {
@@ -555,6 +566,27 @@ impl<F: API> UI<F> {
555566
}
556567
Ok(())
557568
} // Handle help chat in HELP mode
569+
// Handle custom dispatch commands
570+
async fn handle_dispatch_command(&mut self, event_name: &str, event_value: &str) -> Result<()> {
571+
// Initialize the conversation if it doesn't exist
572+
self.init_conversation().await?;
573+
574+
// Log the dispatch
575+
CONSOLE.writeln(
576+
TitleFormat::execute(format!("Dispatching custom event: {}", event_name))
577+
.sub_title(format!("value: {}", event_value))
578+
.format(),
579+
)?;
580+
581+
// Dispatch the event using the API
582+
self.api.dispatch(event_name, event_value).await?;
583+
584+
CONSOLE.writeln(
585+
TitleFormat::success("Event dispatched successfully").format(),
586+
)?;
587+
588+
Ok(())
589+
}
558590
async fn help_chat(&mut self, content: String) -> Result<()> {
559591
let conversation_id = self.init_conversation().await?;
560592

0 commit comments

Comments
 (0)