mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
}
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user