mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
removing at_command
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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<SlashCommand>),
|
||||
File(FileSearchPopup),
|
||||
At(CommandPopup<AtCommand>),
|
||||
}
|
||||
|
||||
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<AtCommand> = 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<AppEvent>, Receiver<AppEvent>) =
|
||||
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)
|
||||
|
||||
@@ -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<SlashCommand> {
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandPopup<AtCommand> {
|
||||
pub(crate) fn at() -> Self {
|
||||
CommandPopup::new('@', built_in_at_commands())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: CommandInfo> WidgetRef for CommandPopup<C> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let matches = self.filtered_commands();
|
||||
@@ -182,10 +167,6 @@ impl<C: CommandInfo> WidgetRef for CommandPopup<C> {
|
||||
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<C: CommandInfo> WidgetRef for CommandPopup<C> {
|
||||
} 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
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user