feat(tui/paste): show [image: filename] placeholder and map to temp path

- Composer now inserts a placeholder instead of a full temp path when an image
  is pasted (from clipboard or base64/data-URI).
- Added AppEvent::RegisterPastedImage; composer notifies ChatWidget to map the
  placeholder to its backing file so submit resolves to LocalImage.
- Keeps visual consistency with drag-and-drop and avoids leaking temp paths in
  the visible message.
This commit is contained in:
James Peter
2025-09-01 20:28:12 +10:00
parent 6f068dc81a
commit d597f0e30f
5 changed files with 37 additions and 5 deletions
+5
View File
@@ -546,6 +546,11 @@ impl App<'_> {
AppEvent::Paste(text) => {
self.dispatch_paste_event(text);
}
AppEvent::RegisterPastedImage { placeholder, path } => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.register_pasted_image(placeholder, path);
}
}
AppEvent::CodexEvent(event) => {
self.dispatch_codex_event(event);
}
+5
View File
@@ -14,6 +14,7 @@ use crate::bottom_pane::chrome_selection_view::ChromeLaunchOption;
use crate::slash_command::SlashCommand;
use codex_protocol::models::ResponseItem;
use std::fmt;
use std::path::PathBuf;
/// Wrapper to allow including non-Debug types in Debug enums without leaking internals.
pub(crate) struct Redacted<T>(pub T);
@@ -127,5 +128,9 @@ pub(crate) enum AppEvent {
prefix_items: Vec<ResponseItem>,
prefill: String,
},
/// Register an image placeholder inserted by the composer with its backing path
/// so ChatWidget can resolve it to a LocalImage on submit.
RegisterPastedImage { placeholder: String, path: PathBuf },
}
+18 -4
View File
@@ -30,6 +30,7 @@ use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::textarea::TextArea;
use crate::bottom_pane::textarea::TextAreaState;
use crate::app_event::AppEvent;
use crate::clipboard_paste::normalize_pasted_path;
use crate::clipboard_paste::paste_image_to_temp_png;
use crate::clipboard_paste::try_decode_base64_image_to_temp_png;
@@ -415,9 +416,16 @@ impl ChatComposer {
let char_count = pasted.chars().count();
// If the pasted text looks like a base64/data-URI image, decode it and insert as a path.
if let Ok((path, info)) = try_decode_base64_image_to_temp_png(&pasted) {
let path_str = path.to_string_lossy();
self.textarea.insert_str(&path_str);
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("image.png");
let placeholder = format!("[image: {}]", filename);
// Insert placeholder and notify chat widget about the mapping.
self.textarea.insert_str(&placeholder);
self.textarea.insert_str(" ");
self.app_event_tx
.send(AppEvent::RegisterPastedImage { placeholder: placeholder.clone(), path });
self.flash_footer_notice(format!("Added image {}x{} (PNG)", info.width, info.height));
} else if char_count > LARGE_PASTE_CHAR_THRESHOLD {
let placeholder = format!("[Pasted Content {char_count} chars]");
@@ -429,9 +437,15 @@ impl ChatComposer {
// No textual content pasted — try reading an image directly from the OS clipboard.
match paste_image_to_temp_png() {
Ok((path, info)) => {
let path_str = path.to_string_lossy();
self.textarea.insert_str(&path_str);
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("image.png");
let placeholder = format!("[image: {}]", filename);
self.textarea.insert_str(&placeholder);
self.textarea.insert_str(" ");
self.app_event_tx
.send(AppEvent::RegisterPastedImage { placeholder: placeholder.clone(), path });
// Give a small visual confirmation in the footer.
self.flash_footer_notice(format!(
"Added image {}x{} (PNG)",
+5
View File
@@ -1322,6 +1322,11 @@ impl ChatWidget<'_> {
self.bottom_pane.insert_str(s);
}
pub(crate) fn register_pasted_image(&mut self, placeholder: String, path: std::path::PathBuf) {
self.pending_images.insert(placeholder, path);
self.request_redraw();
}
fn parse_message_with_images(&mut self, text: String) -> UserMessage {
use std::path::Path;
+4 -1
View File
@@ -127,7 +127,10 @@ pub fn try_decode_base64_image_to_temp_png(pasted: &str) -> Result<(PathBuf, Pas
// Remove whitespace that might be wrapped by terminals
let compact: String = b64.chars().filter(|c| !c.is_whitespace()).collect();
let bytes = base64::decode(compact).map_err(|e| PasteImageError::DecodeFailed(e.to_string()))?;
use base64::Engine as _;
let bytes = base64::engine::general_purpose::STANDARD
.decode(compact)
.map_err(|e| PasteImageError::DecodeFailed(e.to_string()))?;
// Load via `image` crate to get dimensions and normalize to PNG
let dyn_img = image::load_from_memory(&bytes)