Handle image paste from empty paste events (#9049)

Handle image paste on empty paste events.

- Intent: make image paste work in terminals that emit empty paste
events.
- Approach: route paste events through an image-aware handler and read
the clipboard when text is empty.
- That's best effort to detect it. Some terminals don't send the empty
signal.
This commit is contained in:
Ahmed Ibrahim
2026-01-12 23:39:59 -08:00
committed by GitHub
parent cbca43d57a
commit 18b737910c
2 changed files with 44 additions and 20 deletions

View File

@@ -541,7 +541,7 @@ impl App {
// [tui-textarea]: https://github.com/rhysd/tui-textarea/blob/4d18622eeac13b309e0ff6a55a46ac6706da68cf/src/textarea.rs#L782-L783
// [iTerm2]: https://github.com/gnachman/iTerm2/blob/5d0c0d9f68523cbd0494dad5422998964a2ecd8d/sources/iTermPasteHelper.m#L206-L216
let pasted = pasted.replace("\r", "\n");
self.chat_widget.handle_paste(pasted);
self.chat_widget.handle_paste_event(pasted);
}
TuiEvent::Draw => {
self.chat_widget.maybe_post_pending_notification(tui);

View File

@@ -1656,26 +1656,10 @@ impl ChatWidget {
modifiers,
kind: KeyEventKind::Press,
..
} if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
&& c.eq_ignore_ascii_case(&'v') =>
} if c.eq_ignore_ascii_case(&'v')
&& modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
{
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.paste_image_from_clipboard();
return;
}
other if other.kind == KeyEventKind::Press => {
@@ -1738,6 +1722,32 @@ impl ChatWidget {
self.request_redraw();
}
/// Attempt to attach an image from the system clipboard.
///
/// This is a best-effort path used when we receive an empty paste event,
/// which some terminals emit when the clipboard contains non-text data
/// (like images). When the clipboard can't be read or no image exists,
/// surface a helpful follow-up so the user can retry with a file path.
fn paste_image_from_clipboard(&mut self) {
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}. Try saving the image to a file and pasting the file path instead.",
)));
}
}
}
pub(crate) fn composer_text_with_pending(&self) -> String {
self.bottom_pane.composer_text_with_pending()
}
@@ -1992,6 +2002,20 @@ impl ChatWidget {
self.bottom_pane.handle_paste(text);
}
/// Route paste events through image detection.
///
/// Terminals vary in how they represent paste: some emit an empty paste
/// payload when the clipboard isn't text (common for image-only clipboard
/// contents). Treat the empty payload as a hint to attempt a clipboard
/// image read; otherwise, fall back to text handling.
pub(crate) fn handle_paste_event(&mut self, text: String) {
if text.is_empty() {
self.paste_image_from_clipboard();
} else {
self.handle_paste(text);
}
}
// Returns true if caller should skip rendering this frame (a future frame is scheduled).
pub(crate) fn handle_paste_burst_tick(&mut self, frame_requester: FrameRequester) -> bool {
if self.bottom_pane.flush_paste_burst_if_due() {