From ac5705c0ed15b39d2404864d5834c24a852eec48 Mon Sep 17 00:00:00 2001 From: pap Date: Fri, 1 Aug 2025 19:33:58 +0100 Subject: [PATCH] removing at_command --- codex-rs/tui/src/app.rs | 38 +---- codex-rs/tui/src/app_event.rs | 3 - codex-rs/tui/src/at_command.rs | 30 ---- codex-rs/tui/src/bottom_pane/chat_composer.rs | 161 ++---------------- codex-rs/tui/src/bottom_pane/command_popup.rs | 22 +-- codex-rs/tui/src/chatwidget.rs | 11 +- codex-rs/tui/src/lib.rs | 2 - 7 files changed, 22 insertions(+), 245 deletions(-) delete mode 100644 codex-rs/tui/src/at_command.rs diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index de8faa0797..e27b06ab70 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -44,14 +44,12 @@ where crate::clipboard_paste::PasteImageError, >, { - if key_event.code == KeyCode::Char('v') + if key_event.kind == KeyEventKind::Press + && key_event.code == KeyCode::Char('v') && key_event .modifiers .contains(crossterm::event::KeyModifiers::CONTROL) { - tracing::debug!( - "Ctrl+V detected – attempting clipboard image import (shortcut for /image)" - ); match paste_fn() { Ok((path, info)) => { tracing::info!( @@ -423,38 +421,6 @@ impl App<'_> { })); } }, - AppEvent::DispatchAtCommand(at_command) => match at_command { - crate::at_command::AtCommand::ClipboardImage => { - match crate::clipboard_paste::paste_image_to_temp_png() { - Ok((path, info)) => { - tracing::info!( - "at_command_image imported path={:?} width={} height={} format={}", - path, - info.width, - info.height, - info.encoded_format_label - ); - self.app_event_tx.send(AppEvent::AttachImage { - path, - width: info.width, - height: info.height, - format_label: info.encoded_format_label, - }); - } - Err(err) => { - if let AppState::Chat { widget } = &mut self.app_state { - widget.add_background_event(format!( - "image import failed: {err}" - )); - } - } - } - } - crate::at_command::AtCommand::File => { - // Currently file search popup is handled entirely in the chat composer. - // No-op here to keep the match exhaustive. - } - }, AppEvent::StartFileSearch(query) => { self.file_search.on_user_query(query); } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index ce8f0e1b15..77ad986e65 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -3,7 +3,6 @@ use codex_file_search::FileMatch; use crossterm::event::KeyEvent; use ratatui::text::Line; -use crate::at_command::AtCommand; use crate::slash_command::SlashCommand; #[derive(Debug)] @@ -35,8 +34,6 @@ pub(crate) enum AppEvent { /// Dispatch a recognized slash command from the UI (composer) to the app /// layer so it can be handled centrally. DispatchSlashCommand(SlashCommand), - /// Dispatch an @ command. - DispatchAtCommand(AtCommand), /// Kick off an asynchronous file search for the given query (text after /// the `@`). Previous searches may be cancelled by the app layer so there diff --git a/codex-rs/tui/src/at_command.rs b/codex-rs/tui/src/at_command.rs deleted file mode 100644 index 10f56cb63e..0000000000 --- a/codex-rs/tui/src/at_command.rs +++ /dev/null @@ -1,30 +0,0 @@ -use strum::IntoEnumIterator; -use strum_macros::{AsRefStr, EnumIter, EnumString, IntoStaticStr}; - -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, EnumString, EnumIter, AsRefStr, IntoStaticStr, -)] -#[strum(serialize_all = "kebab-case")] -pub enum AtCommand { - // Order is presentation order in @ popup. - ClipboardImage, // import image from clipboard - File, // open file search popup -} - -impl AtCommand { - pub fn description(self) -> &'static str { - match self { - AtCommand::ClipboardImage => { - "Import an image from the system clipboard (can be used with ctrl+v)." - } - AtCommand::File => "Search for a file to insert its path.", - } - } - pub fn command(self) -> &'static str { - self.into() - } -} - -pub fn built_in_at_commands() -> Vec<(&'static str, AtCommand)> { - AtCommand::iter().map(|c| (c.command(), c)).collect() -} diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index a04047dbea..c5eea21b17 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -19,8 +19,6 @@ use tui_textarea::TextArea; use super::chat_composer_history::ChatComposerHistory; use super::command_popup::CommandPopup; use super::file_search_popup::FileSearchPopup; -use crate::at_command::AtCommand; -use crate::at_command::built_in_at_commands; use crate::slash_command::SlashCommand; use crate::app_event::AppEvent; @@ -60,7 +58,6 @@ enum ActivePopup { None, Slash(CommandPopup), File(FileSearchPopup), - At(CommandPopup), } impl ChatComposer<'_> { @@ -97,7 +94,7 @@ impl ChatComposer<'_> { self.textarea.lines().len().max(1) as u16 + match &self.active_popup { ActivePopup::None => 1u16, - ActivePopup::Command(c) => c.calculate_required_height(), + ActivePopup::Slash(c) => c.calculate_required_height(), ActivePopup::File(c) => c.calculate_required_height(), } } @@ -168,12 +165,7 @@ impl ChatComposer<'_> { self.textarea.insert_str(&pasted); } self.sync_slash_command_popup(); - if !matches!(self.active_popup, ActivePopup::Slash(_)) { - self.sync_at_command_popup(); - if !matches!(self.active_popup, ActivePopup::At(_)) { - self.sync_file_search_popup(); - } - } + self.sync_file_search_popup(); true } @@ -221,7 +213,6 @@ impl ChatComposer<'_> { let result = match &mut self.active_popup { ActivePopup::Slash(_) => self.handle_key_event_with_slash_popup(key_event), ActivePopup::File(_) => self.handle_key_event_with_file_popup(key_event), - ActivePopup::At(_) => self.handle_key_event_with_at_popup(key_event), ActivePopup::None => self.handle_key_event_without_popup(key_event), }; @@ -230,13 +221,7 @@ impl ChatComposer<'_> { if matches!(self.active_popup, ActivePopup::Slash(_)) { self.dismissed_file_popup_token = None; } else { - // Try @-command popup next if slash popup not active. - self.sync_at_command_popup(); - if matches!(self.active_popup, ActivePopup::At(_)) { - self.dismissed_file_popup_token = None; - } else { - self.sync_file_search_popup(); - } + self.sync_file_search_popup(); } result @@ -411,73 +396,6 @@ impl ChatComposer<'_> { } } - /// Handle key event when the @-command popup is visible. - fn handle_key_event_with_at_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) { - let ActivePopup::At(popup) = &mut self.active_popup else { - unreachable!() - }; - match key_event.into() { - Input { key: Key::Up, .. } => { - popup.move_up(); - (InputResult::None, true) - } - Input { key: Key::Down, .. } => { - popup.move_down(); - (InputResult::None, true) - } - Input { key: Key::Tab, .. } => { - if let Some(cmd) = popup.selected_command() { - let first_line = self - .textarea - .lines() - .first() - .map(|s| s.as_str()) - .unwrap_or(""); - let starts_with_cmd = first_line - .trim_start() - .starts_with(&format!("@{}", cmd.command())); - if !starts_with_cmd { - self.textarea.select_all(); - self.textarea.cut(); - let _ = self.textarea.insert_str(format!("@{} ", cmd.command())); - } - } - (InputResult::None, true) - } - Input { - key: Key::Enter, - shift: false, - alt: false, - ctrl: false, - } => { - if let Some(cmd) = popup.selected_command() { - match cmd { - AtCommand::ClipboardImage => { - // Dispatch image import request but only remove the @token itself (not entire input). - self.app_event_tx.send(AppEvent::DispatchAtCommand(*cmd)); - self.remove_current_at_token(); - self.active_popup = ActivePopup::None; - return (InputResult::None, true); - } - AtCommand::File => { - // Replace the textarea content with the token so file search logic picks it up. - // Remove only current token then insert @file at cursor. - self.remove_current_at_token(); - // Insert a bare '@' to begin a fresh file query (do not pre-fill with 'file'). - let _ = self.textarea.insert_str("@"); - let file_popup = FileSearchPopup::new(); // starts empty; we will show placeholder until user types. - self.active_popup = ActivePopup::File(file_popup); - self.file_search_mode = true; // mark explicit session - return (InputResult::None, true); - } - } - } - self.handle_key_event_without_popup(key_event) - } - input => self.handle_input_basic(input), - } - } - /// Extract the `@token` that the cursor is currently positioned on, if any. /// /// The returned string **does not** include the leading `@`. @@ -690,6 +608,13 @@ impl ChatComposer<'_> { self.textarea.input(input); let text_after = self.textarea.lines().join("\n"); + // Start/continue an explicit file-search session when the cursor is on an @token. + if Self::current_at_token_allow_empty(&self.textarea).is_some() { + self.file_search_mode = true; + // Allow popup to show for this token. + self.dismissed_file_popup_token = None; + } + // Check if any placeholders were removed and remove their corresponding pending pastes self.pending_pastes .retain(|(placeholder, _)| text_after.contains(placeholder)); @@ -877,44 +802,6 @@ impl ChatComposer<'_> { self.dismissed_file_popup_token = None; } - fn sync_at_command_popup(&mut self) { - if matches!( - self.active_popup, - ActivePopup::Slash(_) | ActivePopup::File(_) - ) { - return; - } - let (row, col) = self.textarea.cursor(); - let line = match self.textarea.lines().get(row) { - Some(l) => l.as_str(), - None => return, - }; - let cursor_byte = cursor_byte_offset(line, col as usize); - let show = if let Some((start, end)) = at_token_bounds(line, cursor_byte, true) { - let body = &line[start + 1..end]; - built_in_at_commands() - .iter() - .any(|(name, _)| name.starts_with(&body.to_ascii_lowercase())) - } else { - false - }; - if show { - let (start, end) = at_token_bounds(line, cursor_byte, true).unwrap(); - let body = &line[start + 1..end]; - let synthetic = format!("@{}", body); - match &mut self.active_popup { - ActivePopup::At(popup) => popup.on_composer_text_change(synthetic), - _ => { - let mut popup: CommandPopup = CommandPopup::at(); - popup.on_composer_text_change(synthetic); - self.active_popup = ActivePopup::At(popup); - } - } - } else if matches!(self.active_popup, ActivePopup::At(_)) { - self.active_popup = ActivePopup::None; - } - } - fn update_border(&mut self, has_focus: bool) { let border_style = if has_focus { Style::default().fg(Color::Cyan) @@ -934,7 +821,7 @@ impl ChatComposer<'_> { impl WidgetRef for &ChatComposer<'_> { fn render_ref(&self, area: Rect, buf: &mut Buffer) { match &self.active_popup { - ActivePopup::Command(popup) => { + ActivePopup::Slash(popup) => { let popup_height = popup.calculate_required_height(); // Split the provided rect so that the popup is rendered at the @@ -1010,23 +897,6 @@ impl WidgetRef for &ChatComposer<'_> { .style(Style::default().dim()) .render_ref(bottom_line_rect, buf); } - ActivePopup::At(popup) => { - let popup_height = popup.calculate_required_height(&area); - let popup_rect = Rect { - x: area.x, - y: area.y, - width: area.width, - height: popup_height.min(area.height), - }; - let textarea_rect = Rect { - x: area.x, - y: area.y + popup_rect.height, - width: area.width, - height: area.height.saturating_sub(popup_rect.height), - }; - popup.render(popup_rect, buf); - self.textarea.render(textarea_rect, buf); - } } } } @@ -1568,7 +1438,7 @@ mod tests { use crossterm::event::KeyModifiers; let (tx, _rx) = std::sync::mpsc::channel(); let sender = AppEventSender::new(tx); - let mut composer = ChatComposer::new(true, sender); + let mut composer = ChatComposer::new(true, sender, false); let path = std::path::PathBuf::from("/tmp/image1.png"); assert!(composer.attach_image(path.clone(), 32, 16, "PNG")); composer.handle_paste(" hi".into()); @@ -1590,7 +1460,7 @@ mod tests { use crossterm::event::KeyModifiers; let (tx, _rx) = std::sync::mpsc::channel(); let sender = AppEventSender::new(tx); - let mut composer = ChatComposer::new(true, sender); + let mut composer = ChatComposer::new(true, sender, false); let path = std::path::PathBuf::from("/tmp/image2.png"); assert!(composer.attach_image(path.clone(), 10, 5, "PNG")); let (result, _) = @@ -1607,7 +1477,7 @@ mod tests { use crossterm::event::KeyModifiers; let (tx, _rx) = std::sync::mpsc::channel(); let sender = AppEventSender::new(tx); - let mut composer = ChatComposer::new(true, sender); + let mut composer = ChatComposer::new(true, sender, false); let path = std::path::PathBuf::from("/tmp/image3.png"); assert!(composer.attach_image(path.clone(), 20, 10, "PNG")); let placeholder = composer.attached_images[0].0.clone(); @@ -1634,7 +1504,6 @@ mod tests { #[test] fn at_clipboard_image_command_triggers_dispatch() { use crate::app_event::AppEvent; - use crate::at_command::AtCommand; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; @@ -1643,7 +1512,7 @@ mod tests { let (tx, rx): (std::sync::mpsc::Sender, Receiver) = std::sync::mpsc::channel(); let sender = AppEventSender::new(tx); - let mut composer = ChatComposer::new(true, sender); + let mut composer = ChatComposer::new(true, sender, false); // Type '@' to open popup composer.handle_key_event(KeyEvent::new(KeyCode::Char('@'), KeyModifiers::NONE)); // Press Enter (should dispatch Image since only option) diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index e7fcd18104..aa99f95317 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -12,7 +12,6 @@ use ratatui::widgets::Table; use ratatui::widgets::Widget; use ratatui::widgets::WidgetRef; -use crate::at_command::{AtCommand, built_in_at_commands}; use crate::slash_command::SlashCommand; use crate::slash_command::built_in_slash_commands; @@ -29,14 +28,6 @@ impl CommandInfo for SlashCommand { SlashCommand::description(*self) } } -impl CommandInfo for AtCommand { - fn command(&self) -> &'static str { - AtCommand::command(*self) - } - fn description(&self) -> &'static str { - AtCommand::description(*self) - } -} const MAX_POPUP_ROWS: usize = 5; /// Ideally this is enough to show the longest command name. @@ -159,12 +150,6 @@ impl CommandPopup { } } -impl CommandPopup { - pub(crate) fn at() -> Self { - CommandPopup::new('@', built_in_at_commands()) - } -} - impl WidgetRef for CommandPopup { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let matches = self.filtered_commands(); @@ -182,10 +167,6 @@ impl WidgetRef for CommandPopup { let command_style = Style::default().fg(Color::LightBlue); for (idx, cmd) in visible_matches.iter().enumerate() { rows.push(Row::new(vec![ -<<<<<<< HEAD - Cell::from(format!("{}{}", self.prefix, cmd.command())).style(cmd_style), - Cell::from(cmd.description().to_string()).style(desc_style), -======= Cell::from(Line::from(vec![ if Some(idx) == self.selected_idx { Span::styled( @@ -195,10 +176,9 @@ impl WidgetRef for CommandPopup { } else { Span::styled(QUADRANT_LEFT_HALF, Style::default().fg(Color::DarkGray)) }, - Span::styled(format!("/{}", cmd.command()), command_style), + Span::styled(format!("{}{}", self.prefix, cmd.command()), command_style), ])), Cell::from(cmd.description().to_string()).style(default_style), ->>>>>>> main ])); } } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c8441348c6..251b7eb0a6 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -195,11 +195,9 @@ impl ChatWidget<'_> { self.bottom_pane .attach_image(path.clone(), width, height, format_label); // Surface a quick background event so user sees confirmation. - self.conversation_history.add_background_event(format!( - "[image copied] {}x{} {}", - width, height, format_label - )); - self.emit_last_history_entry(); + self.add_to_history(HistoryCell::new_background_event(format!( + "[image copied] {width}x{height} {format_label}" + ))); self.request_redraw(); } @@ -535,8 +533,7 @@ impl ChatWidget<'_> { } pub(crate) fn add_background_event(&mut self, msg: String) { - self.conversation_history.add_background_event(msg); - self.emit_last_history_entry(); + self.add_to_history(HistoryCell::new_background_event(msg)); self.request_redraw(); } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 5249708d92..6101e5cefc 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -21,13 +21,11 @@ use tracing_subscriber::prelude::*; mod app; mod app_event; mod app_event_sender; -mod at_command; mod bottom_pane; mod chatwidget; mod citation_regex; mod cli; mod clipboard_paste; -mod conversation_history_widget; mod custom_terminal; mod exec_command; mod file_search;