mirror of
https://github.com/openai/codex.git
synced 2026-05-28 06:55:01 +00:00
@@ -869,8 +869,8 @@ impl ChatComposer {
|
||||
///
|
||||
/// - If the paste is larger than `LARGE_PASTE_CHAR_THRESHOLD` chars, inserts a placeholder
|
||||
/// element (expanded on submit) and stores the full text in `pending_pastes`.
|
||||
/// - Otherwise, if the paste looks like an image path, attaches the image and inserts a
|
||||
/// trailing space so the user can keep typing naturally.
|
||||
/// - Otherwise, if the paste looks like an image path outside shell mode, attaches the image
|
||||
/// and inserts a trailing space so the user can keep typing naturally.
|
||||
/// - Otherwise, inserts the pasted text directly into the textarea.
|
||||
///
|
||||
/// In all cases, clears any paste-burst Enter suppression state so a real paste cannot affect
|
||||
@@ -884,6 +884,7 @@ impl ChatComposer {
|
||||
self.draft.pending_pastes.push((placeholder, pasted));
|
||||
} else if char_count > 1
|
||||
&& self.image_paste_enabled()
|
||||
&& !self.draft.is_bash_mode
|
||||
&& self.handle_paste_image_path(pasted.clone())
|
||||
{
|
||||
self.draft.textarea.insert_str(" ");
|
||||
@@ -9719,6 +9720,36 @@ mod tests {
|
||||
assert_eq!(imgs, vec![tmp_path]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pasting_filepath_in_shell_mode_keeps_literal_text() {
|
||||
let tmp = tempdir().expect("create TempDir");
|
||||
let tmp_path: PathBuf = tmp.path().join("codex_tui_test_paste_image.png");
|
||||
let img: ImageBuffer<Rgba<u8>, Vec<u8>> =
|
||||
ImageBuffer::from_fn(3, 2, |_x, _y| Rgba([1, 2, 3, 255]));
|
||||
img.save(&tmp_path).expect("failed to write temp png");
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
|
||||
type_chars_humanlike(&mut composer, &['!']);
|
||||
assert!(composer.draft.is_bash_mode);
|
||||
|
||||
let path = tmp_path.to_string_lossy().to_string();
|
||||
let needs_redraw = composer.handle_paste(path.clone());
|
||||
|
||||
assert!(needs_redraw);
|
||||
assert_eq!(composer.draft.textarea.text(), path);
|
||||
assert_eq!(composer.current_text(), format!("!{path}"));
|
||||
assert!(composer.local_image_paths().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_path_input_submits_without_command_error() {
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
@@ -284,7 +284,10 @@ use crate::bottom_pane::SelectionItem;
|
||||
use crate::bottom_pane::SelectionViewParams;
|
||||
use crate::bottom_pane::custom_prompt_view::CustomPromptView;
|
||||
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||
use crate::clipboard_paste::PasteImageError;
|
||||
use crate::clipboard_paste::PastedImageInfo;
|
||||
use crate::clipboard_paste::paste_image_to_temp_png;
|
||||
use crate::clipboard_paste::paste_text_or_file_path;
|
||||
use crate::collaboration_modes;
|
||||
use crate::diff_render::display_path_for;
|
||||
use crate::exec_cell::CommandOutput;
|
||||
|
||||
@@ -72,23 +72,7 @@ impl ChatWidget {
|
||||
} if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
|
||||
&& c.eq_ignore_ascii_case(&'v') =>
|
||||
{
|
||||
match paste_image_to_temp_png() {
|
||||
Ok((path, info)) => {
|
||||
tracing::debug!(
|
||||
"pasted image size={}x{} format={}",
|
||||
info.width,
|
||||
info.height,
|
||||
info.encoded_format.label()
|
||||
);
|
||||
self.attach_image(path);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to paste image: {err}");
|
||||
self.add_to_history(history_cell::new_error_event(format!(
|
||||
"Failed to paste image: {err}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
self.handle_paste_image_shortcut(paste_text_or_file_path, paste_image_to_temp_png);
|
||||
return;
|
||||
}
|
||||
other if other.kind == KeyEventKind::Press => {
|
||||
@@ -158,6 +142,41 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_paste_image_shortcut(
|
||||
&mut self,
|
||||
paste_text_or_file_path: impl FnOnce() -> Result<Option<String>, String>,
|
||||
paste_image_to_temp_png: impl FnOnce() -> Result<(PathBuf, PastedImageInfo), PasteImageError>,
|
||||
) {
|
||||
if self.bottom_pane.composer_text().starts_with('!') {
|
||||
match paste_text_or_file_path() {
|
||||
Ok(Some(text)) => self.handle_paste(text),
|
||||
Ok(None) => tracing::debug!(
|
||||
"clipboard did not contain text or a file path to paste in shell mode"
|
||||
),
|
||||
Err(err) => tracing::warn!("failed to paste text in shell mode: {err}"),
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match paste_image_to_temp_png() {
|
||||
Ok((path, info)) => {
|
||||
tracing::debug!(
|
||||
"pasted image size={}x{} format={}",
|
||||
info.width,
|
||||
info.height,
|
||||
info.encoded_format.label()
|
||||
);
|
||||
self.attach_image(path);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to paste image: {err}");
|
||||
self.add_to_history(history_cell::new_error_event(format!(
|
||||
"Failed to paste image: {err}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach a local image to the composer when the active model supports image inputs.
|
||||
///
|
||||
/// When the model does not advertise image support, we keep the draft unchanged and surface a
|
||||
|
||||
@@ -985,6 +985,24 @@ async fn restore_thread_input_state_syncs_sleep_inhibitor_state() {
|
||||
assert!(!chat.bottom_pane.is_task_running());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn paste_image_shortcut_in_shell_mode_pastes_clipboard_text() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.bottom_pane
|
||||
.set_composer_text("!ls ".to_string(), Vec::new(), Vec::new());
|
||||
|
||||
chat.handle_paste_image_shortcut(
|
||||
|| Ok(Some("/tmp/from-clipboard.png".to_string())),
|
||||
|| panic!("shell mode should not read image clipboard"),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
chat.bottom_pane.composer_text(),
|
||||
"!ls /tmp/from-clipboard.png"
|
||||
);
|
||||
assert!(chat.bottom_pane.take_recent_submission_images().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn alt_up_edits_most_recent_queued_message() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -46,6 +46,28 @@ pub struct PastedImageInfo {
|
||||
pub encoded_format: EncodedImageFormat, // Always PNG for now.
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn paste_text_or_file_path() -> Result<Option<String>, String> {
|
||||
let mut cb = arboard::Clipboard::new().map_err(|e| format!("clipboard unavailable: {e}"))?;
|
||||
|
||||
if let Ok(text) = cb.get_text()
|
||||
&& !text.is_empty()
|
||||
{
|
||||
return Ok(Some(text));
|
||||
}
|
||||
|
||||
let files = cb.get().file_list().unwrap_or_default();
|
||||
Ok(files
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|path| path.to_string_lossy().into_owned()))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn paste_text_or_file_path() -> Result<Option<String>, String> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Capture image from system clipboard, encode to PNG, and return bytes + info.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn paste_image_as_png() -> Result<(Vec<u8>, PastedImageInfo), PasteImageError> {
|
||||
|
||||
Reference in New Issue
Block a user