Skip to content

Instantly share code, notes, and snippets.

@NorfeldtKnowit
Last active May 1, 2026 10:51
Show Gist options
  • Select an option

  • Save NorfeldtKnowit/c5e0e8833b0fcac6e4d2f2dbd9322d82 to your computer and use it in GitHub Desktop.

Select an option

Save NorfeldtKnowit/c5e0e8833b0fcac6e4d2f2dbd9322d82 to your computer and use it in GitHub Desktop.
Pre-drafted product.md and tech.md for warpdotdev/warp#9729 — Image preview in the Code Editor file pane

Image preview in the Code Editor file pane

Draft product.md for warpdotdev/warp#9729. To be committed under specs/GH9729/product.md once the issue receives ready-to-spec.

Problem

Clicking an image in Warp's File Tree opens it as a text file in the Code Editor. There is no way to view image assets without leaving Warp.

Goals

  • Native image preview in the existing Code Editor tab system.
  • Lightweight in-directory navigation between images, keyboard-first.
  • Reuse the existing image-decode and EXIF pipeline used for Agent context attachments.

Non-goals

  • Image editing, annotation, drawing.
  • RAW formats (CR2, NEF, ARW, DNG): out of scope for v1; Reveal in Finder/Files is the escape hatch.
  • PDFs: separate feature.
  • Image diffing across git revisions: follow-up.
  • Slideshow / full-screen presentation mode.
  • Remote URL preview; local files only.
  • Terminal-grid inline images: tracked in #1693 and #5286, unrelated surface.

Testable behavior invariants

  1. Clicking a file with extension .png, .jpg, .jpeg, .gif, .webp, .svg, .heic, .heif, .bmp, .tiff, .tif, .avif, or .ico in the File Tree opens an Image Preview tab, not a text editor tab.
  2. Magic-byte sniffing overrides the extension when they disagree. A .png file that is actually JPEG opens correctly. A .png file that is text opens as text with a non-blocking warning.
  3. The Image Preview tab participates in the existing tab system: draggable, splittable, closeable, restorable across sessions, identical keyboard shortcuts for tab navigation.
  4. The main view fits the image to the pane on open, preserving aspect ratio, never upscaling beyond 100% by default.
  5. Cmd+= zooms in by 25 percent steps up to 1600 percent. Cmd+- zooms out by 25 percent steps down to a level where the image fits the pane. Cmd+0 returns to Fit. The keybindings appear in Settings, Keyboard shortcuts and are rebindable.
  6. Trackpad pinch zooms around the cursor. Trackpad two-finger scroll pans when zoomed and is a no-op at Fit. Click-and-drag pans when zoomed.
  7. The status footer shows filename, pixel dimensions, file size in human-readable units, and format string. It is always visible.
  8. Animated GIF, animated WebP, and animated AVIF auto-play on open and loop. A play/pause control is visible and toggleable. The pause state persists when navigating to the next image and back.
  9. SVG renders via vector rasterization at the current zoom level. The rendering remains visually sharp at every zoom step (no fixed-resolution bitmap).
  10. EXIF orientation is honored for both the main view and thumbnails. An image with EXIF rotation 6 displays portrait without manual rotation.
  11. Embedded ICC color profiles are respected on macOS via ColorSync. On Linux and Windows the v1 behavior is documented as best-effort sRGB fallback.
  12. The thumbnail strip lists every supported image file in the same directory as the open image, sorted by case-insensitive natural filename order (so img2.png precedes img10.png).
  13. The current image's thumbnail is visibly selected and the strip auto-scrolls to keep it in view.
  14. Pressing Left or Right arrow with the preview pane focused navigates to the previous or next image in the strip. Home and End jump to first and last. Navigation does not wrap; it stops at ends.
  15. Clicking a thumbnail replaces the current preview in the same tab. Cmd+click on a thumbnail opens it in a new tab.
  16. Thumbnails are decoded on a background thread pool. The strip never blocks the UI; it shows skeleton placeholders until decode completes.
  17. Thumbnails are cached on disk keyed by (canonical path, mtime, size, target dimension). Cache invalidates on mtime change.
  18. The thumbnail strip is virtualized: directories with more than 500 images render only the visible thumbnails plus a small overscan; memory does not grow linearly with directory size.
  19. A corrupt or unreadable image opens with a non-blocking error state showing filename, error reason, and a Reveal in Finder/Files action. The editor pane does not crash and other tabs are unaffected.
  20. The right-click context menu on the preview surface offers, in order: Copy Image, Copy file path (relative), Copy file path (absolute), Reveal in Finder/Files, Attach as Agent context.
  21. Dragging from the preview surface into an Agent prompt input attaches the image as context using the existing image-attach pipeline; no new wire format.
  22. Closing all Image Preview tabs reclaims any decoded full-size image memory. The idle thumbnail cache obeys a configurable size cap (default 256 MB) under Settings, Editor, Image preview.
  23. Nothing about this feature changes terminal escape-sequence handling. The terminal grid path is unaffected.

Open questions

  • Should the thumbnail strip include subdirectory traversal (depth 1, opt-in)? v1 default: no.
  • Should .psd and .xcf get a "supported but flat-composite only" tier? v1 default: no.
  • Should we offer a checkerboard backdrop toggle for transparent images? Strong yes; revisit during design review.
  • Should single-clicking a thumbnail focus-but-not-load (preserving the current preview until Enter or double-click), or load immediately? v1 proposal: load immediately.

Image preview in the Code Editor file pane: tech spec

Draft tech.md for warpdotdev/warp#9729. To be committed under specs/GH9729/tech.md once the issue receives ready-to-spec.

Context

Current system

Code-context confirmed by Oz's automated triage on #9729.

  • File Tree clicks route through app/src/code/file_tree/view.rs::open_file, which emits FileTreeEvent::OpenFile after resolving a FileTarget.
  • The FileTarget enum lives in app/src/util/openable_file_type.rs and currently has variants for MarkdownViewer, CodeEditor, external editor, and system-open. There is no ImagePreview variant. Adding one is the keystone change.
  • File-type classification lives in crates/warp_util/src/file_type.rs. Raster formats (PNG, JPG, GIF, WebP, BMP, TIFF, ICO) are currently classified as binary by extension. SVG is classified as text and routes to the Code Editor as XML, which is the misroute users see today.
  • FileTarget resolution and tab opening is handled in app/src/workspace/view.rs, which currently has branches for MarkdownViewer, CodeEditor, and system/external targets but no branch for an in-Warp media preview.
  • Code Editor tabs are constructed in app/src/code/view.rs via LocalCodeEditorView, backed by shared text buffers from app/src/code/global_buffer_model.rs. The tab model needs either a new pane/tab variant or a routed non-text view analogous to the Markdown viewer path.
  • Image decode utilities for agent-mode attachments live in app/src/util/image.rs. They cover resize and validation for PNG, JPEG, GIF, and WebP MIME types only. This is the seed for a reusable decoder; it is not currently a UI preview/thumbnail pipeline and will need to grow to cover SVG, HEIC/HEIF, BMP, TIFF, AVIF, ICO, and frame iteration for animated formats.
  • Tab state, drag-split-close, and session restore are view-agnostic at the controller layer.

Current routing flow (today)

File Tree click
  → app/src/code/file_tree/view.rs::open_file
  → resolve FileTarget (app/src/util/openable_file_type.rs)
  → emit FileTreeEvent::OpenFile
  → app/src/workspace/view.rs handles target
      ├── MarkdownViewer        (split-pane preview)
      ├── CodeEditor            (text/code, including SVG today, which is the misroute)
      ├── external editor       (configured app)
      └── system-open           (shell out)

This request adds a fifth branch: FileTarget::ImagePreview routing to a new ImagePreviewView in app/src/workspace/view.rs, parallel to the Markdown viewer path.

Files likely touched

Concrete integration points, grounded in Oz's code-context analysis. Subject to refinement during spec PR review.

  • app/src/util/openable_file_type.rs: add FileTarget::ImagePreview; extend resolution rules to map image extensions to it.
  • crates/warp_util/src/file_type.rs: reclassify PNG/JPG/GIF/WebP/BMP/TIFF/AVIF/HEIC/HEIF/ICO from "binary" to a new "image" classification, and reclassify SVG out of text.
  • app/src/code/file_tree/view.rs: no logic changes expected; the existing open_file path already emits FileTreeEvent::OpenFile with whatever FileTarget resolves.
  • app/src/workspace/view.rs: add the FileTarget::ImagePreview branch that constructs an ImagePreviewView tab, modeled on the Markdown viewer branch.
  • app/src/code/view.rs and app/src/code/global_buffer_model.rs: register the new view type in the tab/pane variant model so drag-split-close and session restore work uniformly.
  • app/src/util/image.rs: extend the existing decoder/validation pipeline with SVG (resvg), HEIC/HEIF (libheif on Linux, Image I/O on macOS, WIC on Windows), BMP, TIFF, AVIF, ICO, EXIF orientation, and frame iteration for animated formats. Expose a stable ImageDecoder trait.
  • New app/src/code/image_preview/ module (or sibling of the Markdown viewer): houses ImagePreviewView, ZoomPanController, ThumbnailStripController, ThumbnailCache, ImageThumbStrip, AnimationController.
  • Settings schema and preferences UI: add Editor → Image preview section (thumbnail cache cap, default zoom mode, animation autoplay).
  • Default keybindings JSON: register image_preview.next/prev/first/last/zoom_in/zoom_out/zoom_reset/toggle_animation.
  • Localization strings for control labels and status footer fields.

Proposed changes

Modules and components

  1. MediaTypeRouter: between File Tree click and editor open. Inputs: path. Logic: extension lookup, then a 16-byte magic-byte sniff if the extension claims an image. Outputs: a ViewKind::Text or ViewKind::ImagePreview selector.
  2. ImagePreviewView: the new view. Owns:
    • ImageDecoder (reused from Agent attach).
    • ZoomPanController: zoom level, pan offset, fit-to-pane math, pinch and drag handling.
    • ThumbnailStripController: streams fs::read_dir, filters by supported extensions, natural-sorts, exposes a virtualized index to the strip widget. Watches the directory via the existing File Tree watcher to react to add/remove/rename.
    • AnimationController: frame iteration for animated formats; pause state.
  3. ThumbnailCache: bounded LRU on disk under the platform's app-support directory, keyed by (canonical_path, mtime_ns, size_bytes, target_dim). Background-thread decode, returns a future.
  4. ImageThumbStrip: virtualized horizontal list widget. Renders skeletons during decode, emits selection events.

Format handling

  • Raster (PNG, JPG, GIF, WebP, BMP, TIFF, AVIF, ICO): image-rs crate or whatever the existing Agent attach pipeline uses. AVIF may require libavif linkage; confirm current dependency surface.
  • HEIC / HEIF: macOS via Image I/O. Linux via libheif (packaging note required). Windows via Windows Imaging Component if available, otherwise libheif fallback.
  • SVG: resvg for cross-platform consistency. Re-rasterize on zoom change rather than scaling a bitmap.
  • Animated WebP and AVIF: confirm the decoder supports frame iteration. If not, gate behind a follow-up flag and treat the first frame as static for v1.

Keyboard, mouse, and trackpad

  • Wire arrow-key navigation through the existing keybinding system. Reuse the same focus model as text editor tabs.
  • Trackpad: pinch maps to zoom around the cursor; two-finger scroll maps to pan when zoomed, no-op at Fit. Momentum follows platform native.

Memory and performance

  • Decode full-size on demand, drop on tab close. Cap concurrent full-size decodes at 1 per tab.
  • Thumbnail target dimension: 96 px on the long edge by default, 2x for HiDPI. Configurable.
  • Thumbnail cache cap: 256 MB default. LRU eviction.
  • Strip virtualization: render visible plus 5 overscan on each side. Decode requests cancel when scrolled past.
  • Thumbnail decode thread pool capped at min(num_cpus, 4).

Settings and keybindings

  • New Settings section: Editor, Image preview. Fields: thumbnail cache cap (MB), default zoom mode (Fit / 100%), animation autoplay (on/off).
  • New keybindings: image_preview.next, image_preview.prev, image_preview.first, image_preview.last, image_preview.zoom_in, image_preview.zoom_out, image_preview.zoom_reset, image_preview.toggle_animation.

Drag and context menu

  • Drag source emits the same payload type as the Agent context attach drag. Sink unchanged.
  • Context menu entries reuse existing actions where possible (Reveal in Finder, Copy path, Attach as Agent context). New entry: Copy Image, which writes the decoded bitmap to the system pasteboard in PNG.

Testing and validation

Unit

  • MediaTypeRouter: extension and magic-byte combinations, including mismatches and zero-byte files.
  • ThumbnailCache: key correctness across mtime and size changes, LRU eviction, concurrent gets.
  • Natural-sort ordering: parity with Finder natural sort on a fixture set including img1, img2, img10, IMG_001, mixed case, unicode.
  • ZoomPanController: fit math, clamp behavior at zoom limits, pan clamp at viewport edges.
  • EXIF orientation: matrix of 8 EXIF rotations against a fixture image set.

Integration

  • Click image in File Tree, expect ImagePreview tab.
  • Click corrupt image, expect error state, no crash, neighbors still navigable.
  • Open directory with 2000 PNGs, expect first paint under 100 ms with cold cache and stable memory.
  • Animated GIF: open, observe loop, pause, navigate next, return; pause persists.
  • SVG: zoom to 800 percent, no pixelation.
  • HEIC on each platform.

Cross-platform smoke

  • macOS arm64, macOS x86_64, Linux x86_64, Windows x86_64.

Risks

  • HEIC on Linux drags libheif into the dependency graph. Confirm license compatibility and packaging story before commit.
  • AVIF animated decode may not be supported by the current pipeline. v1 may degrade to first-frame for animated AVIF.
  • Color profile fidelity differs across platforms. Documented as best-effort outside macOS for v1.
  • Watching directories with thousands of files for change events can be expensive. Reuse the existing File Tree watcher rather than instantiating a second one.

Follow-ups

  • Image diff for VCS-tracked changes, two-up or onion-skin.
  • "Open with" external app dispatch once the policy decision in #2206 is revisited.
  • RAW formats behind a flag.
  • Markdown viewer image embeds could share the renderer.
  • Slideshow / full-screen presentation mode.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment