diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index a97948f3e..4a0adb8de 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -366,6 +366,11 @@ impl App<'_> { widget.add_diff_output(text); } } + SlashCommand::Mention => { + if let AppState::Chat { widget } = &mut self.app_state { + widget.insert_str("@"); + } + } SlashCommand::Status => { if let AppState::Chat { widget } = &mut self.app_state { widget.add_status_output(); diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 09ff8b7ab..78506f572 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -198,6 +198,12 @@ impl ChatComposer { self.set_has_focus(has_focus); } + pub(crate) fn insert_str(&mut self, text: &str) { + self.textarea.insert_str(text); + self.sync_command_popup(); + self.sync_file_search_popup(); + } + /// Handle a key event coming from the main UI. pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) { let result = match &mut self.active_popup { @@ -1078,6 +1084,46 @@ mod tests { } } + #[test] + fn slash_mention_dispatches_command_and_inserts_at() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + use std::sync::mpsc::TryRecvError; + + let (tx, rx) = std::sync::mpsc::channel(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new(true, sender, false); + + for ch in ['/', 'm', 'e', 'n', 't', 'i', 'o', 'n'] { + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE)); + } + + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + match result { + InputResult::None => {} + InputResult::Submitted(text) => { + panic!("expected command dispatch, but composer submitted literal text: {text}") + } + } + assert!(composer.textarea.is_empty(), "composer should be cleared"); + + match rx.try_recv() { + Ok(AppEvent::DispatchCommand(cmd)) => { + assert_eq!(cmd.command(), "mention"); + composer.insert_str("@"); + } + Ok(_other) => panic!("unexpected app event"), + Err(TryRecvError::Empty) => panic!("expected a DispatchCommand event for '/mention'"), + Err(TryRecvError::Disconnected) => { + panic!("app event channel disconnected") + } + } + assert_eq!(composer.textarea.text(), "@"); + } + #[test] fn test_multiple_pastes_submission() { use crossterm::event::KeyCode; diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 0c8610470..4606f9b8e 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -196,6 +196,11 @@ impl BottomPane<'_> { } } + pub(crate) fn insert_str(&mut self, text: &str) { + self.composer.insert_str(text); + self.request_redraw(); + } + /// Update the status indicator text. Prefer replacing the composer with /// the StatusIndicatorView so the input pane shows a single-line status /// like: `▌ Working waiting for model`. diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index d03a51bd7..173ab64af 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -676,6 +676,10 @@ impl ChatWidget<'_> { self.submit_user_message(text.into()); } + pub(crate) fn insert_str(&mut self, text: &str) { + self.bottom_pane.insert_str(text); + } + pub(crate) fn token_usage(&self) -> &TokenUsage { &self.total_token_usage } diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index e58ab8521..0de1f6fae 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -16,6 +16,7 @@ pub enum SlashCommand { Init, Compact, Diff, + Mention, Status, Prompts, Logout, @@ -33,6 +34,7 @@ impl SlashCommand { SlashCommand::Compact => "summarize conversation to prevent hitting the context limit", SlashCommand::Quit => "exit Codex", SlashCommand::Diff => "show git diff (including untracked files)", + SlashCommand::Mention => "mention a file", SlashCommand::Status => "show current session configuration and token usage", SlashCommand::Prompts => "show example prompts", SlashCommand::Logout => "log out of Codex",