mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(tui/images): harden image drop + clipped rendering
This commit is contained in:
@@ -5918,9 +5918,35 @@ impl ChatWidget<'_> {
|
||||
// Removed: pending insert sequencing is not used under strict ordering.
|
||||
|
||||
pub(crate) fn register_pasted_image(&mut self, placeholder: String, path: std::path::PathBuf) {
|
||||
let persisted = self.persist_user_image_if_needed(&path).unwrap_or(path);
|
||||
self.pending_images.insert(placeholder, persisted);
|
||||
self.request_redraw();
|
||||
let persisted = self
|
||||
.persist_user_image_if_needed(&path)
|
||||
.unwrap_or_else(|| path.clone());
|
||||
if persisted.exists() && persisted.is_file() {
|
||||
self.pending_images.insert(placeholder, persisted);
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
// Some terminals (notably on macOS) can drop a "promised" file path
|
||||
// (e.g., from Preview) that doesn't actually exist on disk. Fall back
|
||||
// to reading the image directly from the clipboard.
|
||||
match crate::clipboard_paste::paste_image_to_temp_png() {
|
||||
Ok((clipboard_path, _info)) => {
|
||||
let clipboard_persisted = self
|
||||
.persist_user_image_if_needed(&clipboard_path)
|
||||
.unwrap_or(clipboard_path);
|
||||
self.pending_images.insert(placeholder, clipboard_persisted);
|
||||
self.push_background_tail("Used clipboard image (dropped file path was missing).");
|
||||
self.request_redraw();
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"dropped/pasted image path missing ({}); clipboard fallback failed: {}",
|
||||
persisted.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn persist_user_image_if_needed(&self, path: &std::path::Path) -> Option<PathBuf> {
|
||||
@@ -6003,14 +6029,30 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let placeholder = mat.as_str();
|
||||
if placeholder.starts_with("[image:") {
|
||||
if let Some(path) = self.pending_images.remove(placeholder) {
|
||||
// Emit the placeholder marker verbatim followed by the image so the LLM sees placement
|
||||
ordered_items.push(InputItem::Text {
|
||||
text: placeholder.to_string(),
|
||||
});
|
||||
ordered_items.push(InputItem::LocalImage { path });
|
||||
if path.exists() && path.is_file() {
|
||||
// Emit the placeholder marker verbatim followed by the image so the LLM sees placement
|
||||
ordered_items.push(InputItem::Text {
|
||||
text: placeholder.to_string(),
|
||||
});
|
||||
ordered_items.push(InputItem::LocalImage { path });
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"pending image placeholder {} resolved to missing path {}",
|
||||
placeholder,
|
||||
path.display()
|
||||
);
|
||||
self.push_background_tail(format!(
|
||||
"Dropped image file went missing; not attaching ({})",
|
||||
path.display()
|
||||
));
|
||||
ordered_items.push(InputItem::Text {
|
||||
text: placeholder.to_string(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Unknown placeholder: preserve as text
|
||||
ordered_items.push(InputItem::Text {
|
||||
|
||||
@@ -23,7 +23,7 @@ use ratatui::prelude::Style;
|
||||
use ratatui::text::{Line, Text};
|
||||
use ratatui::widgets::{Paragraph, Widget, Wrap};
|
||||
use ratatui_image::{Image, Resize};
|
||||
use ratatui_image::picker::Picker;
|
||||
use ratatui_image::picker::{Picker, ProtocolType};
|
||||
use ratatui_image::FilterType;
|
||||
use image::ImageReader;
|
||||
use std::cell::RefCell;
|
||||
@@ -1134,14 +1134,23 @@ impl BrowserSessionCell {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let path = Path::new(path_str);
|
||||
let rows_to_copy = (visible_bottom - visible_top) as u16;
|
||||
if rows_to_copy == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let dest_x = area.x + accent_width + left_pad;
|
||||
let dest_y = area.y + (visible_top - viewport_top) as u16;
|
||||
let placeholder_area = Rect {
|
||||
x: dest_x,
|
||||
y: dest_y,
|
||||
width: screenshot_width,
|
||||
height: rows_to_copy,
|
||||
};
|
||||
|
||||
if !path.exists() {
|
||||
let placeholder_area = Rect {
|
||||
x: area.x + accent_width + left_pad,
|
||||
y: area.y,
|
||||
width: screenshot_width,
|
||||
height: layout.height_rows.min(area.height as usize) as u16,
|
||||
};
|
||||
self.render_screenshot_placeholder(path, placeholder_area, buf);
|
||||
return;
|
||||
}
|
||||
@@ -1151,28 +1160,44 @@ impl BrowserSessionCell {
|
||||
return;
|
||||
}
|
||||
|
||||
let picker = self.ensure_picker();
|
||||
let supports_partial_render = matches!(picker.protocol_type(), ProtocolType::Halfblocks);
|
||||
let is_partially_visible = visible_top != shot_top || visible_bottom != shot_bottom;
|
||||
if is_partially_visible && !supports_partial_render {
|
||||
self.render_screenshot_placeholder(path, placeholder_area, buf);
|
||||
return;
|
||||
}
|
||||
|
||||
if !is_partially_visible {
|
||||
let protocol_target = Rect::new(0, 0, screenshot_width, full_height);
|
||||
if self.ensure_protocol(path, protocol_target, &picker).is_err() {
|
||||
self.render_screenshot_placeholder(path, placeholder_area, buf);
|
||||
return;
|
||||
}
|
||||
let dest_target = Rect::new(dest_x, dest_y, screenshot_width, full_height);
|
||||
if let Some((_, _, protocol)) = self.cached_image_protocol.borrow_mut().as_mut() {
|
||||
let image = Image::new(protocol);
|
||||
image.render(dest_target, buf);
|
||||
} else {
|
||||
self.render_screenshot_placeholder(path, placeholder_area, buf);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if !supports_partial_render {
|
||||
self.render_screenshot_placeholder(path, placeholder_area, buf);
|
||||
return;
|
||||
}
|
||||
|
||||
let offscreen = match self.render_screenshot_buffer(path, screenshot_width, full_height) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(_) => {
|
||||
let placeholder_area = Rect {
|
||||
x: area.x + accent_width + left_pad,
|
||||
y: area.y,
|
||||
width: screenshot_width,
|
||||
height: layout.height_rows.min(area.height as usize) as u16,
|
||||
};
|
||||
self.render_screenshot_placeholder(path, placeholder_area, buf);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let src_start_row = (visible_top - shot_top) as u16;
|
||||
let rows_to_copy = (visible_bottom - visible_top) as u16;
|
||||
if rows_to_copy == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let dest_x = area.x + accent_width + left_pad;
|
||||
let dest_y = area.y + (visible_top - viewport_top) as u16;
|
||||
let area_bottom = area.y + area.height;
|
||||
let area_right = area.x + area.width;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ use ::image::ImageReader;
|
||||
use ::image::image_dimensions;
|
||||
use ratatui::widgets::{Paragraph, Wrap};
|
||||
use ratatui_image::{Image, Resize};
|
||||
use ratatui_image::picker::Picker;
|
||||
use ratatui_image::picker::{Picker, ProtocolType};
|
||||
use ratatui_image::FilterType;
|
||||
use std::cell::RefCell;
|
||||
use std::path::Path;
|
||||
@@ -525,6 +525,7 @@ impl ImageOutputCell {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let rows_to_copy = (visible_bottom - visible_top) as u16;
|
||||
if rows_to_copy == 0 {
|
||||
return;
|
||||
@@ -532,9 +533,14 @@ impl ImageOutputCell {
|
||||
|
||||
let dest_x = area.x + accent_width + left_pad;
|
||||
let dest_y = area.y + (visible_top - viewport_top) as u16;
|
||||
let placeholder_area = Rect {
|
||||
x: dest_x,
|
||||
y: dest_y,
|
||||
width: image_width,
|
||||
height: rows_to_copy,
|
||||
};
|
||||
|
||||
if !path.exists() {
|
||||
let placeholder_area = Rect { x: dest_x, y: dest_y, width: image_width, height: rows_to_copy };
|
||||
self.render_image_placeholder(path, placeholder_area, buf);
|
||||
return;
|
||||
}
|
||||
@@ -544,10 +550,43 @@ impl ImageOutputCell {
|
||||
return;
|
||||
}
|
||||
|
||||
let picker = self.ensure_picker();
|
||||
let supports_partial_render = matches!(picker.protocol_type(), ProtocolType::Halfblocks);
|
||||
let is_partially_visible = visible_top != shot_top || visible_bottom != shot_bottom;
|
||||
if is_partially_visible && !supports_partial_render {
|
||||
// Graphical terminal protocols (kitty/sixel/iterm2) render with escape sequences that
|
||||
// can't be safely truncated mid-image without risking cursor movement/overdraw.
|
||||
// Prefer a placeholder over broken scroll/clipping.
|
||||
self.render_image_placeholder(path, placeholder_area, buf);
|
||||
return;
|
||||
}
|
||||
|
||||
// For full-image rendering, draw directly into the final buffer so protocols that rely on
|
||||
// escape sequences behave correctly.
|
||||
if !is_partially_visible {
|
||||
let protocol_target = Rect::new(0, 0, image_width, full_height);
|
||||
if self.ensure_protocol(path, protocol_target, &picker).is_err() {
|
||||
self.render_image_placeholder(path, placeholder_area, buf);
|
||||
return;
|
||||
}
|
||||
let dest_target = Rect::new(dest_x, dest_y, image_width, full_height);
|
||||
if let Some((_, _, protocol)) = self.cached_image_protocol.borrow_mut().as_mut() {
|
||||
let image = Image::new(protocol);
|
||||
image.render(dest_target, buf);
|
||||
} else {
|
||||
self.render_image_placeholder(path, placeholder_area, buf);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if !supports_partial_render {
|
||||
self.render_image_placeholder(path, placeholder_area, buf);
|
||||
return;
|
||||
}
|
||||
|
||||
let offscreen = match self.render_image_buffer(path, image_width, full_height) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(_) => {
|
||||
let placeholder_area = Rect { x: dest_x, y: dest_y, width: image_width, height: rows_to_copy };
|
||||
self.render_image_placeholder(path, placeholder_area, buf);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user