Show image previews in the fullscreen transcript (#54216)

## What changed

Render local viewed images and generated image artifacts as bounded thumbnails in the rich fullscreen transcript using Kitty or Sixel graphics. Viewed images appear when their activity is expanded or the detailed presentation is active; generated images appear alongside their saved-path entry.

Keep previews clipped to the viewport, retain graphics across unrelated redraws, and remove them when overlays open. Bound image decoding and cache memory, and preserve text-only entries for remote workspaces, unsupported terminals, missing files, and oversized images. Image rows do not enter searchable or selectable text.

## Testing

Add layout and snapshot coverage for disclosure, viewport clipping, rewrapping, raw-mode fallback, deleted files, and oversized rasters. Add terminal coverage for retained Kitty graphics, repainting after a clear, local-file transport, and overlay cleanup.

GitOrigin-RevId: fb2ec146331acfcfc82b2b283616671c9ee93757
This commit is contained in:
Eric Traut
2026-10-15 07:00:34 +00:00
committed by copyberry
parent bb64b894fd
commit dffa0deb69
24 changed files with 631 additions and 47 deletions
+33
View File
@@ -89,6 +89,7 @@ struct VisibleRow {
/// Shared scrolling and interaction state for compact and detailed transcript presentations.
pub(crate) struct TranscriptView {
pub(crate) image_protocol: Option<crate::pets::ImageProtocol>,
pub(crate) mouse_scroll_speed: f64,
pending_mouse_scroll: f64,
pub(crate) copy_on_select: bool,
@@ -128,6 +129,7 @@ pub(crate) struct TranscriptView {
impl Default for TranscriptView {
fn default() -> Self {
Self {
image_protocol: None,
mouse_scroll_speed: 1.0,
pending_mouse_scroll: 0.0,
copy_on_select: false,
@@ -305,6 +307,37 @@ impl TranscriptView {
self.render_search(buf);
}
pub(crate) fn image_placements(&self) -> Vec<crate::transcript_image::ImagePlacement> {
let mut placements = Vec::new();
let mut y = self.area.y;
for rows in self.visible.chunk_by(|a, b| a.index == b.index) {
let entry_y = y;
y += rows.len() as u16;
let visible = &rows[0];
let Some(image) = visible.layout.visible_image() else {
continue;
};
let image_start = visible.layout.text_row_count();
let skip = image_start.saturating_sub(visible.row);
if skip >= rows.len() {
continue;
}
let area = Rect::new(
self.area.x + self.area.width.saturating_sub(image.columns) / 2,
entry_y + skip as u16,
image.columns,
(rows.len() - skip) as u16,
);
placements.push(crate::transcript_image::ImagePlacement {
image: Arc::clone(image),
area,
first_row: visible.row.saturating_sub(image_start),
file: None,
});
}
placements
}
pub(crate) fn sync_live_tail(
&mut self,
width: u16,