From b9f260057c39749fa1438ce283f602ae13edb90f Mon Sep 17 00:00:00 2001 From: jif-oai Date: Wed, 19 Nov 2025 14:21:04 +0000 Subject: [PATCH] Optional command --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 23 ++++++++++++------- codex-rs/tui/src/chatwidget.rs | 12 +++++++--- codex-rs/tui/src/chatwidget/tests.rs | 16 ++++++------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 531d5a9227..6a7a574268 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -69,7 +69,10 @@ const LARGE_PASTE_CHAR_THRESHOLD: usize = 1000; #[derive(Debug, PartialEq)] pub enum InputResult { Submitted(String), - Command { command: SlashCommand, args: String }, + Command { + command: SlashCommand, + args: Option, + }, None, } @@ -333,13 +336,17 @@ impl ChatComposer { PasteBurst::recommended_flush_delay() } - fn command_args_from_line(line: &str, command: SlashCommand) -> String { + fn command_args_from_line(line: &str, command: SlashCommand) -> Option { if let Some((name, rest)) = parse_slash_name(line) && name == command.command() { - return rest.to_string(); + let trimmed = rest.trim(); + if trimmed.is_empty() { + return None; + } + return Some(trimmed.to_string()); } - String::new() + None } /// Integrate results from an asynchronous file search. @@ -2410,7 +2417,7 @@ mod tests { match result { InputResult::Command { command: cmd, args } => { assert_eq!(cmd.command(), "init"); - assert!(args.is_empty()); + assert!(args.is_none()); } InputResult::Submitted(text) => { panic!("expected command dispatch, but composer submitted literal text: {text}") @@ -2486,7 +2493,7 @@ mod tests { match result { InputResult::Command { command: cmd, args } => { assert_eq!(cmd.command(), "diff"); - assert!(args.is_empty()); + assert!(args.is_none()); } InputResult::Submitted(text) => { panic!("expected command dispatch after Tab completion, got literal submit: {text}") @@ -2520,7 +2527,7 @@ mod tests { match result { InputResult::Command { command: cmd, args } => { assert_eq!(cmd, SlashCommand::Save); - assert_eq!(args, "feature-one"); + assert_eq!(args.as_deref(), Some("feature-one")); } InputResult::Submitted(text) => { panic!( @@ -2556,7 +2563,7 @@ mod tests { match result { InputResult::Command { command: cmd, args } => { assert_eq!(cmd.command(), "mention"); - assert!(args.is_empty()); + assert!(args.is_none()); } InputResult::Submitted(text) => { panic!("expected command dispatch, but composer submitted literal text: {text}") diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index fe956afd4c..3e209baffb 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1323,7 +1323,7 @@ impl ChatWidget { self.request_redraw(); } - fn dispatch_command(&mut self, cmd: SlashCommand, args: String) { + fn dispatch_command(&mut self, cmd: SlashCommand, args: Option) { if !cmd.available_during_task() && self.bottom_pane.is_task_running() { let message = format!( "'/{}' is disabled while a task is in progress.", @@ -1463,8 +1463,14 @@ impl ChatWidget { } } - fn handle_save_command(&mut self, args: String) { - let name = args.trim(); + fn handle_save_command(&mut self, args: Option) { + let Some(name_raw) = args else { + self.add_to_history(history_cell::new_error_event( + "Usage: /save ".to_string(), + )); + return; + }; + let name = name_raw.trim(); if name.is_empty() { self.add_to_history(history_cell::new_error_event( "Usage: /save ".to_string(), diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index c20d237efc..2a0ced7564 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -954,7 +954,7 @@ fn slash_init_skips_when_project_doc_exists() { std::fs::write(&existing_path, "existing instructions").unwrap(); chat.config.cwd = tempdir.path().to_path_buf(); - chat.dispatch_command(SlashCommand::Init, String::new()); + chat.dispatch_command(SlashCommand::Init, None); match op_rx.try_recv() { Err(TryRecvError::Empty) => {} @@ -982,7 +982,7 @@ fn slash_init_skips_when_project_doc_exists() { fn slash_quit_requests_exit() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); - chat.dispatch_command(SlashCommand::Quit, String::new()); + chat.dispatch_command(SlashCommand::Quit, None); assert_matches!(rx.try_recv(), Ok(AppEvent::ExitRequest)); } @@ -991,7 +991,7 @@ fn slash_quit_requests_exit() { fn slash_exit_requests_exit() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); - chat.dispatch_command(SlashCommand::Exit, String::new()); + chat.dispatch_command(SlashCommand::Exit, None); assert_matches!(rx.try_recv(), Ok(AppEvent::ExitRequest)); } @@ -1000,7 +1000,7 @@ fn slash_exit_requests_exit() { fn slash_undo_sends_op() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); - chat.dispatch_command(SlashCommand::Undo, String::new()); + chat.dispatch_command(SlashCommand::Undo, None); match rx.try_recv() { Ok(AppEvent::CodexOp(Op::Undo)) => {} @@ -1014,7 +1014,7 @@ fn slash_rollout_displays_current_path() { let rollout_path = PathBuf::from("/tmp/codex-test-rollout.jsonl"); chat.current_rollout_path = Some(rollout_path.clone()); - chat.dispatch_command(SlashCommand::Rollout, String::new()); + chat.dispatch_command(SlashCommand::Rollout, None); let cells = drain_insert_history(&mut rx); assert_eq!(cells.len(), 1, "expected info message for rollout path"); @@ -1029,7 +1029,7 @@ fn slash_rollout_displays_current_path() { fn slash_rollout_handles_missing_path() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); - chat.dispatch_command(SlashCommand::Rollout, String::new()); + chat.dispatch_command(SlashCommand::Rollout, None); let cells = drain_insert_history(&mut rx); assert_eq!( @@ -1624,7 +1624,7 @@ fn feedback_selection_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); // Open the feedback category selection popup via slash command. - chat.dispatch_command(SlashCommand::Feedback, String::new()); + chat.dispatch_command(SlashCommand::Feedback, None); let popup = render_bottom_popup(&chat, 80); assert_snapshot!("feedback_selection_popup", popup); @@ -1702,7 +1702,7 @@ fn disabled_slash_command_while_task_running_snapshot() { chat.bottom_pane.set_task_running(true); // Dispatch a command that is unavailable while a task runs (e.g., /model) - chat.dispatch_command(SlashCommand::Model, String::new()); + chat.dispatch_command(SlashCommand::Model, None); // Drain history and snapshot the rendered error line(s) let cells = drain_insert_history(&mut rx);