From 87c23a5495c276c0f9d4249f2470fb2d361877c9 Mon Sep 17 00:00:00 2001 From: Peter Miller Date: Sun, 25 Jan 2026 07:31:37 -0800 Subject: [PATCH 1/2] feat(git): add image detection and base64 encoding for diff viewer --- src-tauri/Cargo.lock | 17 +-- src-tauri/Cargo.toml | 1 + src-tauri/src/git.rs | 242 ++++++++++++++++++++++++++++--------- src-tauri/src/git_utils.rs | 32 +++++ src-tauri/src/types.rs | 24 ++++ src/types.ts | 6 + 6 files changed, 260 insertions(+), 62 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d0f18ea44..1b351032f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -601,6 +601,7 @@ dependencies = [ name = "codex-monitor" version = "0.1.0" dependencies = [ + "base64 0.22.1", "block2", "chrono", "cpal", @@ -950,7 +951,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1124,7 +1125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1735,11 +1736,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "home" -version = "0.5.12" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3865,7 +3866,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4918,7 +4919,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5728,7 +5729,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 02350b180..fadaaa99f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,6 +32,7 @@ tokio = { version = "1", features = ["fs", "net", "io-util", "process", "rt", "s uuid = { version = "1", features = ["v4"] } tauri-plugin-dialog = "2" git2 = "0.20.3" +base64 = "0.22" fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } ignore = "0.4.25" portable-pty = "0.8" diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index d29ee1123..d68ec50f2 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -1,5 +1,7 @@ +use std::fs; use std::path::{Path, PathBuf}; +use base64::{engine::general_purpose::STANDARD, Engine as _}; use git2::{BranchType, DiffOptions, Repository, Sort, Status, StatusOptions}; use serde_json::json; use tauri::State; @@ -7,7 +9,7 @@ use tokio::process::Command; use crate::git_utils::{ checkout_branch, commit_to_entry, diff_patch_to_string, diff_stats_for_path, - list_git_roots as scan_git_roots, parse_github_repo, resolve_git_root, + image_mime_type, list_git_roots as scan_git_roots, parse_github_repo, resolve_git_root, }; use crate::state::AppState; use crate::types::{ @@ -18,6 +20,30 @@ use crate::types::{ use crate::utils::normalize_git_path; const INDEX_SKIP_WORKTREE_FLAG: u16 = 0x4000; +const MAX_IMAGE_BYTES: usize = 10 * 1024 * 1024; + +fn encode_image_base64(data: &[u8]) -> Option { + if data.len() > MAX_IMAGE_BYTES { + return None; + } + Some(STANDARD.encode(data)) +} + +fn blob_to_base64(blob: git2::Blob) -> Option { + if blob.size() > MAX_IMAGE_BYTES { + return None; + } + encode_image_base64(blob.content()) +} + +fn read_image_base64(path: &Path) -> Option { + let metadata = fs::metadata(path).ok()?; + if metadata.len() > MAX_IMAGE_BYTES as u64 { + return None; + } + let data = fs::read(path).ok()?; + encode_image_base64(&data) +} async fn run_git_command(repo_root: &Path, args: &[&str]) -> Result<(), String> { let output = Command::new("git") @@ -392,6 +418,7 @@ pub(crate) async fn get_git_status( .get(&workspace_id) .ok_or("workspace not found")? .clone(); + drop(workspaces); let repo_root = resolve_git_root(&entry)?; let repo = Repository::open(&repo_root).map_err(|e| e.to_string())?; @@ -725,57 +752,113 @@ pub(crate) async fn get_git_diffs( .clone(); let repo_root = resolve_git_root(&entry)?; - let repo = Repository::open(&repo_root).map_err(|e| e.to_string())?; - let head_tree = repo - .head() - .ok() - .and_then(|head| head.peel_to_tree().ok()); - - let mut options = DiffOptions::new(); - options - .include_untracked(true) - .recurse_untracked_dirs(true) - .show_untracked_content(true); + tokio::task::spawn_blocking(move || { + let repo = Repository::open(&repo_root).map_err(|e| e.to_string())?; + let head_tree = repo + .head() + .ok() + .and_then(|head| head.peel_to_tree().ok()); + + let mut options = DiffOptions::new(); + options + .include_untracked(true) + .recurse_untracked_dirs(true) + .show_untracked_content(true); + + let diff = match head_tree.as_ref() { + Some(tree) => repo + .diff_tree_to_workdir_with_index(Some(tree), Some(&mut options)) + .map_err(|e| e.to_string())?, + None => repo + .diff_tree_to_workdir_with_index(None, Some(&mut options)) + .map_err(|e| e.to_string())?, + }; - let diff = match head_tree.as_ref() { - Some(tree) => repo - .diff_tree_to_workdir_with_index(Some(tree), Some(&mut options)) - .map_err(|e| e.to_string())?, - None => repo - .diff_tree_to_workdir_with_index(None, Some(&mut options)) - .map_err(|e| e.to_string())?, - }; + let mut results = Vec::new(); + for (index, delta) in diff.deltas().enumerate() { + let old_path = delta.old_file().path(); + let new_path = delta.new_file().path(); + let display_path = new_path.or(old_path); + let Some(display_path) = display_path else { + continue; + }; + let old_path_str = old_path.map(|path| path.to_string_lossy()); + let new_path_str = new_path.map(|path| path.to_string_lossy()); + let display_path_str = display_path.to_string_lossy(); + let normalized_path = normalize_git_path(&display_path_str); + let old_image_mime = old_path_str.as_deref().and_then(image_mime_type); + let new_image_mime = new_path_str.as_deref().and_then(image_mime_type); + let is_image = old_image_mime.is_some() || new_image_mime.is_some(); + + if is_image { + let is_deleted = delta.status() == git2::Delta::Deleted; + let is_added = delta.status() == git2::Delta::Added; + + let old_image_data = if !is_added && old_image_mime.is_some() { + head_tree + .as_ref() + .and_then(|tree| old_path.and_then(|path| tree.get_path(path).ok())) + .and_then(|entry| repo.find_blob(entry.id()).ok()) + .and_then(blob_to_base64) + } else { + None + }; + + let new_image_data = if !is_deleted && new_image_mime.is_some() { + match new_path { + Some(path) => { + let full_path = repo_root.join(path); + read_image_base64(&full_path) + } + None => None, + } + } else { + None + }; + + results.push(GitFileDiff { + path: normalized_path, + diff: String::new(), + is_binary: true, + is_image: true, + old_image_data, + new_image_data, + old_image_mime: old_image_mime.map(str::to_string), + new_image_mime: new_image_mime.map(str::to_string), + }); + continue; + } - let mut results = Vec::new(); - for (index, delta) in diff.deltas().enumerate() { - let path = delta - .new_file() - .path() - .or_else(|| delta.old_file().path()); - let Some(path) = path else { - continue; - }; - let patch = match git2::Patch::from_diff(&diff, index) { - Ok(patch) => patch, - Err(_) => continue, - }; - let Some(mut patch) = patch else { - continue; - }; - let content = match diff_patch_to_string(&mut patch) { - Ok(content) => content, - Err(_) => continue, - }; - if content.trim().is_empty() { - continue; + let patch = match git2::Patch::from_diff(&diff, index) { + Ok(patch) => patch, + Err(_) => continue, + }; + let Some(mut patch) = patch else { + continue; + }; + let content = match diff_patch_to_string(&mut patch) { + Ok(content) => content, + Err(_) => continue, + }; + if content.trim().is_empty() { + continue; + } + results.push(GitFileDiff { + path: normalized_path, + diff: content, + is_binary: false, + is_image: false, + old_image_data: None, + new_image_data: None, + old_image_mime: None, + new_image_mime: None, + }); } - results.push(GitFileDiff { - path: normalize_git_path(path.to_string_lossy().as_ref()), - diff: content, - }); - } - Ok(results) + Ok(results) + }) + .await + .map_err(|e| e.to_string())? } #[tauri::command] @@ -789,6 +872,7 @@ pub(crate) async fn get_git_log( .get(&workspace_id) .ok_or("workspace not found")? .clone(); + drop(workspaces); let repo_root = resolve_git_root(&entry)?; let repo = Repository::open(&repo_root).map_err(|e| e.to_string())?; @@ -915,13 +999,57 @@ pub(crate) async fn get_git_commit_diff( let mut results = Vec::new(); for (index, delta) in diff.deltas().enumerate() { - let path = delta - .new_file() - .path() - .or_else(|| delta.old_file().path()); - let Some(path) = path else { + let old_path = delta.old_file().path(); + let new_path = delta.new_file().path(); + let display_path = new_path.or(old_path); + let Some(display_path) = display_path else { continue; }; + let old_path_str = old_path.map(|path| path.to_string_lossy()); + let new_path_str = new_path.map(|path| path.to_string_lossy()); + let display_path_str = display_path.to_string_lossy(); + let normalized_path = normalize_git_path(&display_path_str); + let old_image_mime = old_path_str.as_deref().and_then(image_mime_type); + let new_image_mime = new_path_str.as_deref().and_then(image_mime_type); + let is_image = old_image_mime.is_some() || new_image_mime.is_some(); + + if is_image { + let is_deleted = delta.status() == git2::Delta::Deleted; + let is_added = delta.status() == git2::Delta::Added; + + let old_image_data = if !is_added && old_image_mime.is_some() { + parent_tree + .as_ref() + .and_then(|tree| old_path.and_then(|path| tree.get_path(path).ok())) + .and_then(|entry| repo.find_blob(entry.id()).ok()) + .and_then(blob_to_base64) + } else { + None + }; + + let new_image_data = if !is_deleted && new_image_mime.is_some() { + new_path + .and_then(|path| commit_tree.get_path(path).ok()) + .and_then(|entry| repo.find_blob(entry.id()).ok()) + .and_then(blob_to_base64) + } else { + None + }; + + results.push(GitCommitDiff { + path: normalized_path, + status: status_for_delta(delta.status()).to_string(), + diff: String::new(), + is_binary: true, + is_image: true, + old_image_data, + new_image_data, + old_image_mime: old_image_mime.map(str::to_string), + new_image_mime: new_image_mime.map(str::to_string), + }); + continue; + } + let patch = match git2::Patch::from_diff(&diff, index) { Ok(patch) => patch, Err(_) => continue, @@ -937,9 +1065,15 @@ pub(crate) async fn get_git_commit_diff( continue; } results.push(GitCommitDiff { - path: normalize_git_path(path.to_string_lossy().as_ref()), + path: normalized_path, status: status_for_delta(delta.status()).to_string(), diff: content, + is_binary: false, + is_image: false, + old_image_data: None, + new_image_data: None, + old_image_mime: None, + new_image_mime: None, }); } diff --git a/src-tauri/src/git_utils.rs b/src-tauri/src/git_utils.rs index 0118bb672..69d431726 100644 --- a/src-tauri/src/git_utils.rs +++ b/src-tauri/src/git_utils.rs @@ -7,6 +7,23 @@ use ignore::WalkBuilder; use crate::types::{GitLogEntry, WorkspaceEntry}; use crate::utils::normalize_git_path; +pub(crate) fn image_mime_type(path: &str) -> Option<&'static str> { + let ext = Path::new(path) + .extension() + .and_then(|value| value.to_str())? + .to_ascii_lowercase(); + match ext.as_str() { + "png" => Some("image/png"), + "jpg" | "jpeg" => Some("image/jpeg"), + "gif" => Some("image/gif"), + "webp" => Some("image/webp"), + "svg" => Some("image/svg+xml"), + "bmp" => Some("image/bmp"), + "ico" => Some("image/x-icon"), + _ => None, + } +} + pub(crate) fn commit_to_entry(commit: git2::Commit) -> GitLogEntry { let summary = commit.summary().unwrap_or("").to_string(); let author = commit.author().name().unwrap_or("").to_string(); @@ -71,6 +88,21 @@ pub(crate) fn diff_patch_to_string(patch: &mut git2::Patch) -> Result Option { let trimmed = remote_url.trim(); if trimmed.is_empty() { diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index ca529eeee..010b0dbd5 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -12,6 +12,18 @@ pub(crate) struct GitFileStatus { pub(crate) struct GitFileDiff { pub(crate) path: String, pub(crate) diff: String, + #[serde(default, rename = "isBinary")] + pub(crate) is_binary: bool, + #[serde(default, rename = "isImage")] + pub(crate) is_image: bool, + #[serde(rename = "oldImageData")] + pub(crate) old_image_data: Option, + #[serde(rename = "newImageData")] + pub(crate) new_image_data: Option, + #[serde(rename = "oldImageMime")] + pub(crate) old_image_mime: Option, + #[serde(rename = "newImageMime")] + pub(crate) new_image_mime: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -19,6 +31,18 @@ pub(crate) struct GitCommitDiff { pub(crate) path: String, pub(crate) status: String, pub(crate) diff: String, + #[serde(default, rename = "isBinary")] + pub(crate) is_binary: bool, + #[serde(default, rename = "isImage")] + pub(crate) is_image: bool, + #[serde(rename = "oldImageData")] + pub(crate) old_image_data: Option, + #[serde(rename = "newImageData")] + pub(crate) new_image_data: Option, + #[serde(rename = "oldImageMime")] + pub(crate) old_image_mime: Option, + #[serde(rename = "newImageMime")] + pub(crate) new_image_mime: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src/types.ts b/src/types.ts index 93fc7f94e..939b0fa6b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -204,6 +204,12 @@ export type GitCommitDiff = { path: string; status: string; diff: string; + isBinary?: boolean; + isImage?: boolean; + oldImageData?: string | null; + newImageData?: string | null; + oldImageMime?: string | null; + newImageMime?: string | null; }; export type GitLogEntry = { From 7a5f8446d3ce975a33626549c8b1c22099221c51 Mon Sep 17 00:00:00 2001 From: Peter Miller Date: Sun, 25 Jan 2026 07:31:46 -0800 Subject: [PATCH 2/2] feat(git): add image preview support in diff viewer --- src/features/git/components/GitDiffViewer.tsx | 28 +++- src/features/git/components/ImageDiffCard.tsx | 157 ++++++++++++++++++ src/features/git/hooks/useGitDiffs.ts | 20 ++- src/features/layout/hooks/useLayoutNodes.tsx | 5 + src/styles/diff-viewer.css | 72 ++++++++ src/types.ts | 6 + 6 files changed, 277 insertions(+), 11 deletions(-) create mode 100644 src/features/git/components/ImageDiffCard.tsx diff --git a/src/features/git/components/GitDiffViewer.tsx b/src/features/git/components/GitDiffViewer.tsx index 68a13a3e3..72562f17e 100644 --- a/src/features/git/components/GitDiffViewer.tsx +++ b/src/features/git/components/GitDiffViewer.tsx @@ -7,11 +7,17 @@ import { workerFactory } from "../../../utils/diffsWorker"; import type { GitHubPullRequest, GitHubPullRequestComment } from "../../../types"; import { formatRelativeTime } from "../../../utils/time"; import { Markdown } from "../../messages/components/Markdown"; +import { ImageDiffCard } from "./ImageDiffCard"; type GitDiffViewerItem = { path: string; status: string; diff: string; + isImage?: boolean; + oldImageData?: string | null; + newImageData?: string | null; + oldImageMime?: string | null; + newImageMime?: string | null; }; type GitDiffViewerProps = { @@ -625,11 +631,23 @@ export function GitDiffViewer({ transform: `translate3d(0, ${virtualRow.start}px, 0)`, }} > - + {entry.isImage ? ( + + ) : ( + + )} ); })} diff --git a/src/features/git/components/ImageDiffCard.tsx b/src/features/git/components/ImageDiffCard.tsx new file mode 100644 index 000000000..e05e8a0fb --- /dev/null +++ b/src/features/git/components/ImageDiffCard.tsx @@ -0,0 +1,157 @@ +import { memo, useMemo } from "react"; +import ImageOff from "lucide-react/dist/esm/icons/image-off"; + +type ImageDiffCardProps = { + path: string; + status: string; + oldImageData?: string | null; + newImageData?: string | null; + oldImageMime?: string | null; + newImageMime?: string | null; + isSelected: boolean; +}; + +function getImageMimeType(path: string): string { + const lower = path.toLowerCase(); + if (lower.endsWith(".png")) return "image/png"; + if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; + if (lower.endsWith(".gif")) return "image/gif"; + if (lower.endsWith(".webp")) return "image/webp"; + if (lower.endsWith(".svg")) return "image/svg+xml"; + if (lower.endsWith(".bmp")) return "image/bmp"; + if (lower.endsWith(".ico")) return "image/x-icon"; + return "image/png"; +} + +function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +export const ImageDiffCard = memo(function ImageDiffCard({ + path, + status, + oldImageData, + newImageData, + oldImageMime, + newImageMime, + isSelected, +}: ImageDiffCardProps) { + const oldDataUri = useMemo( + () => { + if (!oldImageData) return null; + const mimeType = oldImageMime ?? getImageMimeType(path); + return `data:${mimeType};base64,${oldImageData}`; + }, + [oldImageData, oldImageMime, path], + ); + + const newDataUri = useMemo( + () => { + if (!newImageData) return null; + const mimeType = newImageMime ?? getImageMimeType(path); + return `data:${mimeType};base64,${newImageData}`; + }, + [newImageData, newImageMime, path], + ); + + const oldSize = useMemo(() => { + if (!oldImageData) return null; + const bytes = Math.ceil((oldImageData.length * 3) / 4); + return formatFileSize(bytes); + }, [oldImageData]); + + const newSize = useMemo(() => { + if (!newImageData) return null; + const bytes = Math.ceil((newImageData.length * 3) / 4); + return formatFileSize(bytes); + }, [newImageData]); + + const isAdded = status === "A"; + const isDeleted = status === "D"; + const isModified = !isAdded && !isDeleted; + const placeholderLabel = "Image preview unavailable."; + const renderPlaceholder = () => ( +
+ +
{placeholderLabel}
+
+ ); + + return ( +
+
+ + {status} + + {path} +
+
+ {isModified && ( +
+
+ {oldDataUri ? ( + Previous version + ) : ( + renderPlaceholder() + )} + {oldSize &&
{oldSize}
} +
+
+ {newDataUri ? ( + Current version + ) : ( + renderPlaceholder() + )} + {newSize &&
{newSize}
} +
+
+ )} + {isAdded && ( +
+
+ {newDataUri ? ( + New image + ) : ( + renderPlaceholder() + )} + {newSize &&
{newSize}
} +
+
+ )} + {isDeleted && ( +
+
+ {oldDataUri ? ( + Deleted image + ) : ( + renderPlaceholder() + )} + {oldSize &&
{oldSize}
} +
+
+ )} +
+
+ ); +}); diff --git a/src/features/git/hooks/useGitDiffs.ts b/src/features/git/hooks/useGitDiffs.ts index f5059b863..8bcfd8236 100644 --- a/src/features/git/hooks/useGitDiffs.ts +++ b/src/features/git/hooks/useGitDiffs.ts @@ -98,13 +98,21 @@ export function useGitDiffs( const orderedDiffs = useMemo(() => { const diffByPath = new Map( - state.diffs.map((entry) => [entry.path, entry.diff]), + state.diffs.map((entry) => [entry.path, entry]), ); - return files.map((file) => ({ - path: file.path, - status: file.status, - diff: diffByPath.get(file.path) ?? "", - })); + return files.map((file) => { + const entry = diffByPath.get(file.path); + return { + path: file.path, + status: file.status, + diff: entry?.diff ?? "", + isImage: entry?.isImage, + oldImageData: entry?.oldImageData, + newImageData: entry?.newImageData, + oldImageMime: entry?.oldImageMime, + newImageMime: entry?.newImageMime, + }; + }); }, [files, state.diffs]); return { diff --git a/src/features/layout/hooks/useLayoutNodes.tsx b/src/features/layout/hooks/useLayoutNodes.tsx index 773a6e889..64a3f5163 100644 --- a/src/features/layout/hooks/useLayoutNodes.tsx +++ b/src/features/layout/hooks/useLayoutNodes.tsx @@ -61,6 +61,11 @@ type GitDiffViewerItem = { path: string; status: string; diff: string; + isImage?: boolean; + oldImageData?: string | null; + newImageData?: string | null; + oldImageMime?: string | null; + newImageMime?: string | null; }; type WorktreeRenameState = { diff --git a/src/styles/diff-viewer.css b/src/styles/diff-viewer.css index 7484319d7..143008986 100644 --- a/src/styles/diff-viewer.css +++ b/src/styles/diff-viewer.css @@ -576,3 +576,75 @@ right: 12px; z-index: 2; } + +.diff-viewer-item-image { + background: transparent; +} + +.image-diff-content { + padding: 16px; +} + +.image-diff-side-by-side { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.image-diff-single { + display: flex; + justify-content: center; +} + +.image-diff-single .image-diff-pane { + max-width: 50%; +} + +.image-diff-pane { + display: flex; + flex-direction: column; + align-items: center; +} + +.image-diff-preview { + max-width: 100%; + max-height: 300px; + object-fit: contain; + background: repeating-conic-gradient( + var(--surface-control) 0% 25%, + var(--surface-strong) 0% 50% + ) 50% / 16px 16px; +} + +.image-diff-placeholder { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 6px; + min-height: 140px; + padding: 12px 16px; + color: var(--text-subtle); + font-size: 12px; + text-align: center; + border-radius: 8px; + background: var(--surface-strong); + border: 1px solid var(--border-subtle); +} + +.image-diff-placeholder-icon { + width: 20px; + height: 20px; + color: var(--text-faint); +} + +.image-diff-placeholder-text { + line-height: 1.4; +} + +.image-diff-meta { + font-size: 11px; + color: var(--text-faint); + font-variant-numeric: tabular-nums; + margin-top: 8px; +} diff --git a/src/types.ts b/src/types.ts index 939b0fa6b..e01b35ee1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -198,6 +198,12 @@ export type GitFileStatus = { export type GitFileDiff = { path: string; diff: string; + isBinary?: boolean; + isImage?: boolean; + oldImageData?: string | null; + newImageData?: string | null; + oldImageMime?: string | null; + newImageMime?: string | null; }; export type GitCommitDiff = {