Skip to content

feat: add image diff viewer with classic, highlight, and slider modes#412

Merged
matt2e merged 8 commits intomainfrom
image-diff
Mar 19, 2026
Merged

feat: add image diff viewer with classic, highlight, and slider modes#412
matt2e merged 8 commits intomainfrom
image-diff

Conversation

@matt2e
Copy link
Contributor

@matt2e matt2e commented Mar 19, 2026

Image.diff.mov

Summary

  • Add ImageDiffViewer component with three viewing modes: classic (side-by-side), highlight (overlay with differences emphasized), and slider (interactive before/after comparison)
  • Extend git-diff backend to detect and handle binary/image files, passing base64-encoded image data to the frontend
  • Integrate image diff viewer into the existing DiffViewer component, automatically activating for image file changes

Test plan

  • Verify image diffs display correctly in classic, highlight, and slider modes
  • Confirm slider handle is draggable and keyboard-accessible with arrow keys
  • Check that non-image diffs continue to render as before
  • Test with added, removed, and modified image files

🤖 Generated with Claude Code

matt2e and others added 5 commits March 19, 2026 15:40
…, and slider modes

Replace the generic "Binary file" placeholder with a rich image diff viewer
for png, jpg, jpeg, gif, and webp files. The backend now detects image files
by extension, base64-encodes them (up to 5 MB), and sends them as a new
ImageBase64 FileContent variant. The frontend renders three viewing modes:
side-by-side comparison, pixel-difference highlighting via Canvas API, and
a draggable slider overlay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e when single image

- Rename "Side-by-side" mode to "Classic"
- Center the mode toolbar buttons
- Hide the toolbar entirely when only one side exists (add or delete)
- Remove the bottom border from the toolbar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lider handle

- Add draggable="false" to slider images to prevent browser image drag
  and text selection during slider interaction
- Replace dash icon (- -) with left/right arrow triangles on the slider
  handle for a clearer resize affordance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the slider divider from inside the aspect-ratio-constrained image
container to the full-height slider container, and remove vertical
padding in slider mode so the line extends from the toolbar to the
bottom of the viewer area.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keep images constrained to 70vh while the divider line extends to the
full container height.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matt2e matt2e requested review from baxen and wesbillman as code owners March 19, 2026 05:20
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

let output = cli::run(repo, &["show", &spec]).map_err(|e| match e {
GitError::CommandFailed(msg) if msg.contains("does not exist") => {
GitError::CommandFailed(format!("File not found: {path}"))
}
other => other,
})?;

P2 Badge Avoid UTF-8 decoding before loading committed reference images

get_file_at_ref() now tries to classify git show output as image data, but cli::run() in crates/git-diff/src/cli.rs decodes stdout with String::from_utf8 first. For any committed image blob (HEAD:path/to/foo.png, older revisions, etc.), this call returns InvalidUtf8 before is_binary()/bytes_to_image_or_binary() run, so reference-image loading still only works from WORKDIR and fails for normal committed refs in Differ/Staged.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 317 to 318
Err(e) if is_utf8_parse_error(&e) => Ok(Some(git::File {
path: path.to_string(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle non-UTF8 remote image blobs without downgrading them

For remote branches, run_remote_git() ultimately returns a String, so git show <ref>:<image> fails before file_content_from_text() ever sees the raw bytes. This fallback turns every non-UTF8 PNG/JPEG/GIF/WebP into plain Binary, which means the new image viewer never works for remote image diffs or remote reference files in Staged; those cases still render as the old binary notice.

Useful? React with 👍 / 👎.

Comment on lines +92 to +95
export function isImageDiff(diff: FileDiff): boolean {
const beforeImage = diff.before?.content.type === 'ImageBase64';
const afterImage = diff.after?.content.type === 'ImageBase64';
return beforeImage || afterImage;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat partially previewable image diffs as binary, not add/delete

This helper returns true when either side is ImageBase64, even if the other side exists but fell back to Binary (for example because it exceeded IMAGE_PREVIEW_MAX_BYTES). In that case DiffViewer takes the image branch and ImageDiffViewer shows the missing side as “No previous version”/“File deleted”, which misrepresents modified large images as adds or deletes instead of showing a binary-diff fallback.

Useful? React with 👍 / 👎.

matt2e and others added 3 commits March 19, 2026 18:48
When viewing diffs for remote branches (Blox workspace projects),
`git show <ref>:<path>` returns binary data for image files. The previous
`ws_exec` / `run_workspace_git` path called `String::from_utf8` on the
raw stdout, failing silently with a `Binary` fallback instead of an
`ImageBase64` preview.

Fix by adding a `ws_exec_bytes` variant in blox-cli (and wiring it
through blox.rs / branches.rs) that returns `Vec<u8>` without UTF-8
validation. `load_remote_file_at_ref` in diff_commands.rs now uses this
bytes path for `git show`, then passes the raw bytes through the existing
`file_content_from_bytes` helper, which detects binary content, checks
for a known image extension, and base64-encodes the data into an
`ImageBase64` FileContent — the same representation used for local
worktree branches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matt2e matt2e merged commit 6836586 into main Mar 19, 2026
6 checks passed
@matt2e matt2e deleted the image-diff branch March 19, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant