From a36ade39277b78be54c50f631a7b0d449785e64e Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sat, 17 Jan 2026 20:40:06 +0100 Subject: [PATCH 1/4] feat: integrate pierre diffs and optimize scroll refactor: drop git diff virtualization --- package-lock.json | 202 +++++++ package.json | 3 +- src/App.tsx | 10 +- src/features/git/components/GitDiffViewer.tsx | 500 ++++++++++-------- src/features/layout/hooks/useLayoutNodes.tsx | 2 +- src/styles/base.css | 4 + src/styles/diff-viewer.css | 23 +- src/styles/main.css | 1 + src/utils/diffsWorker.ts | 5 + vite.config.ts | 3 + 10 files changed, 510 insertions(+), 243 deletions(-) create mode 100644 src/utils/diffsWorker.ts diff --git a/package-lock.json b/package-lock.json index 51fdb73c4..9fa13cec5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "codex-monitor", "version": "0.7.1", "dependencies": { + "@pierre/diffs": "^1.0.6", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-opener": "^2", @@ -998,6 +999,25 @@ "node": ">= 8" } }, + "node_modules/@pierre/diffs": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@pierre/diffs/-/diffs-1.0.6.tgz", + "integrity": "sha512-VoFvtGCSPi+wAj4Ap+0j80/KPnQbYZsvztjIP4nHsjCgcVUBF7+X1nHFqjl/xbTqSc0okZG1tHYb49CUON9ZXQ==", + "license": "apache-2.0", + "dependencies": { + "@shikijs/core": "^3.0.0", + "@shikijs/engine-javascript": "^3.0.0", + "@shikijs/transformers": "^3.0.0", + "diff": "8.0.2", + "hast-util-to-html": "9.0.5", + "lru_map": "0.4.1", + "shiki": "^3.0.0" + }, + "peerDependencies": { + "react": "^18.3.1 || ^19.0.0", + "react-dom": "^18.3.1 || ^19.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1355,6 +1375,83 @@ "win32" ] }, + "node_modules/@shikijs/core": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz", + "integrity": "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.21.0.tgz", + "integrity": "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.21.0.tgz", + "integrity": "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.21.0.tgz", + "integrity": "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.21.0.tgz", + "integrity": "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.21.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.21.0.tgz", + "integrity": "sha512-CZwvCWWIiRRiFk9/JKzdEooakAP8mQDtBOQ1TKiCaS2E1bYtyBCOkUzS8akO34/7ufICQ29oeSfkb3tT5KtrhA==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.21.0", + "@shikijs/types": "3.21.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.21.0.tgz", + "integrity": "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, "node_modules/@tauri-apps/api": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz", @@ -2684,6 +2781,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3787,6 +3893,29 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -3837,6 +3966,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4557,6 +4696,12 @@ "loose-envify": "cli.js" } }, + "node_modules/lru_map": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.4.1.tgz", + "integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5639,6 +5784,23 @@ "wrappy": "1" } }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6011,6 +6173,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -6366,6 +6552,22 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz", + "integrity": "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.21.0", + "@shikijs/engine-javascript": "3.21.0", + "@shikijs/engine-oniguruma": "3.21.0", + "@shikijs/langs": "3.21.0", + "@shikijs/themes": "3.21.0", + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", diff --git a/package.json b/package.json index 553d2e9d7..f0d78bd7e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "tauri:build": "npm run doctor:strict && tauri build" }, "dependencies": { + "@pierre/diffs": "^1.0.6", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-opener": "^2", @@ -36,9 +37,9 @@ "@types/prismjs": "^1.26.5", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.6.0", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", + "@vitejs/plugin-react": "^4.6.0", "eslint": "^8.57.0", "eslint-plugin-react": "^7.36.1", "eslint-plugin-react-hooks": "^4.6.2", diff --git a/src/App.tsx b/src/App.tsx index be1a1ae59..3c4a8cf9b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -627,12 +627,6 @@ function MainApp() { } } - function handleActiveDiffPath(path: string) { - if (path !== selectedDiffPath) { - setSelectedDiffPath(path); - } - } - function handleDiffLineReference(reference: DiffLineReference) { const startLine = reference.newLine ?? reference.oldLine; const endLine = @@ -741,13 +735,14 @@ function MainApp() { terminalOpen, onDebug: addDebugEntry, }); + const isDefaultScale = Math.abs(uiScale - 1) < 0.001; const appClassName = `app ${isCompact ? "layout-compact" : "layout-desktop"}${ isPhone ? " layout-phone" : "" }${isTablet ? " layout-tablet" : ""}${ reduceTransparency ? " reduced-transparency" : "" }${!isCompact && sidebarCollapsed ? " sidebar-collapsed" : ""}${ !isCompact && rightPanelCollapsed ? " right-panel-collapsed" : "" - }`; + }${isDefaultScale ? " ui-scale-default" : ""}`; const { sidebarNode, messagesNode, @@ -910,7 +905,6 @@ function MainApp() { gitDiffLoading: isDiffLoading, gitDiffError: diffError, onDiffLineReference: handleDiffLineReference, - onDiffActivePathChange: handleActiveDiffPath, onSend: handleSend, onQueue: queueMessage, onStop: interruptTurn, diff --git a/src/features/git/components/GitDiffViewer.tsx b/src/features/git/components/GitDiffViewer.tsx index 3f4291f4f..7e7f71eb0 100644 --- a/src/features/git/components/GitDiffViewer.tsx +++ b/src/features/git/components/GitDiffViewer.tsx @@ -1,9 +1,14 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { DiffBlock } from "./DiffBlock"; -import { parseDiff } from "../../../utils/diff"; -import { languageFromPath } from "../../../utils/syntax"; +import { memo, useCallback, useEffect, useMemo, useState } from "react"; +import { FileDiff, WorkerPoolContextProvider } from "@pierre/diffs/react"; +import type { + FileDiffMetadata, + Hunk, + SelectedLineRange, + AnnotationSide, +} from "@pierre/diffs"; +import { parsePatchFiles } from "@pierre/diffs"; import type { DiffLineReference } from "../../../types"; -import type { ParsedDiffLine } from "../../../utils/diff"; +import { workerFactory } from "../../../utils/diffsWorker"; type GitDiffViewerItem = { path: string; @@ -25,147 +30,219 @@ type SelectedRange = { start: number; end: number; anchor: number; + side?: AnnotationSide; + endSide?: AnnotationSide; }; -type SelectableDiffLine = ParsedDiffLine & { - type: "add" | "del" | "context"; +type LineMaps = { + oldLines: Map; + newLines: Map; }; -function isSelectableLine(line: ParsedDiffLine): line is SelectableDiffLine { - return line.type === "add" || line.type === "del" || line.type === "context"; -} +type ParsedDiffEntry = GitDiffViewerItem & { + fileDiff: FileDiffMetadata | null; + lineMaps: LineMaps | null; +}; -export function GitDiffViewer({ - diffs, - selectedPath, - isLoading, - error, - onLineReference, - onActivePathChange, -}: GitDiffViewerProps) { - const containerRef = useRef(null); - const itemRefs = useRef(new Map()); - const lastScrolledPath = useRef(null); - const lastActivePath = useRef(null); - const skipAutoScroll = useRef(null); - const scrollFrame = useRef(null); - const scrollLock = useRef<{ path: string; expiresAt: number } | null>(null); - const [selectedRange, setSelectedRange] = useState(null); +const DIFF_SCROLL_CSS = ` +[data-column-number], +[data-buffer], +[data-separator-wrapper], +[data-annotation-content] { + position: static !important; +} - useEffect(() => { - lastActivePath.current = selectedPath; - }, [selectedPath]); +[data-buffer] { + background-image: none !important; +} +`; - useEffect(() => { - if (!selectedPath) { - return; - } - if (skipAutoScroll.current) { - const skipPath = skipAutoScroll.current; - skipAutoScroll.current = null; - if (skipPath === selectedPath) { - return; - } - } - if (lastScrolledPath.current === selectedPath) { - return; - } - const target = itemRefs.current.get(selectedPath); - if (target) { - target.scrollIntoView({ behavior: "smooth", block: "start" }); - lastScrolledPath.current = selectedPath; - scrollLock.current = { - path: selectedPath, - expiresAt: performance.now() + 900, - }; - } - }, [selectedPath, diffs.length]); +function normalizePatchName(name: string) { + if (!name) { + return name; + } + return name.replace(/^(?:a|b)\//, ""); +} - const updateActivePath = useCallback(() => { - const container = containerRef.current; - if (!container || !onActivePathChange) { - return; - } - const containerRect = container.getBoundingClientRect(); - const anchorTop = containerRect.top + 12; - if (scrollLock.current) { - const now = performance.now(); - const lock = scrollLock.current; - if (now >= lock.expiresAt) { - scrollLock.current = null; +function buildLineMaps(hunks: Hunk[]): LineMaps { + const oldLines = new Map(); + const newLines = new Map(); + for (const hunk of hunks) { + let oldLine = hunk.deletionStart; + let newLine = hunk.additionStart; + for (const content of hunk.hunkContent) { + if (content.type === "context") { + for (const line of content.lines) { + oldLines.set(oldLine, line); + newLines.set(newLine, line); + oldLine += 1; + newLine += 1; + } } else { - const lockedNode = itemRefs.current.get(lock.path); - if (!lockedNode) { - scrollLock.current = null; - } else { - const rect = lockedNode.getBoundingClientRect(); - const reachedTarget = - rect.top <= anchorTop + 2 && rect.bottom >= containerRect.top; - if (reachedTarget) { - scrollLock.current = null; - } + for (const line of content.deletions) { + oldLines.set(oldLine, line); + oldLine += 1; } - } - } - if (scrollLock.current) { - return; - } - let above: { path: string; top: number } | null = null; - let below: { path: string; top: number } | null = null; - - for (const [path, node] of itemRefs.current.entries()) { - const rect = node.getBoundingClientRect(); - const isVisible = rect.bottom > containerRect.top && rect.top < containerRect.bottom; - if (!isVisible) { - continue; - } - if (rect.top <= anchorTop) { - if (!above || rect.top > above.top) { - above = { path, top: rect.top }; + for (const line of content.additions) { + newLines.set(newLine, line); + newLine += 1; } - } else if (!below || rect.top < below.top) { - below = { path, top: rect.top }; } } + } + return { oldLines, newLines }; +} - const nextPath = above?.path ?? below?.path ?? null; - if (!nextPath || nextPath === lastActivePath.current) { - return; - } - lastActivePath.current = nextPath; - if (nextPath !== selectedPath) { - skipAutoScroll.current = nextPath; - onActivePathChange(nextPath); - } - }, [onActivePathChange, selectedPath]); +function selectionTypeFromSide(side?: AnnotationSide, endSide?: AnnotationSide) { + if (side && endSide && side !== endSide) { + return "mixed"; + } + if (side === "additions" || endSide === "additions") { + return "add"; + } + if (side === "deletions" || endSide === "deletions") { + return "del"; + } + return "context"; +} - useEffect(() => { - const container = containerRef.current; - if (!container || !onActivePathChange) { - return; +function collectSelectedLines( + range: SelectedLineRange, + lineMaps: LineMaps, +) { + const start = Math.min(range.start, range.end); + const end = Math.max(range.start, range.end); + const useNew = range.side === "additions" || range.endSide === "additions"; + const useOld = range.side === "deletions" || range.endSide === "deletions"; + const lines: string[] = []; + for (let lineNumber = start; lineNumber <= end; lineNumber += 1) { + const line = useNew + ? lineMaps.newLines.get(lineNumber) + : useOld + ? lineMaps.oldLines.get(lineNumber) + : lineMaps.newLines.get(lineNumber) ?? lineMaps.oldLines.get(lineNumber); + if (line !== undefined) { + lines.push(line); } - const handleScroll = () => { - if (scrollFrame.current !== null) { - return; - } - scrollFrame.current = window.requestAnimationFrame(() => { - scrollFrame.current = null; - updateActivePath(); - }); - }; + } + return lines; +} - handleScroll(); - container.addEventListener("scroll", handleScroll, { passive: true }); - window.addEventListener("resize", handleScroll); - return () => { - container.removeEventListener("scroll", handleScroll); - window.removeEventListener("resize", handleScroll); - if (scrollFrame.current !== null) { - window.cancelAnimationFrame(scrollFrame.current); - scrollFrame.current = null; - } - }; - }, [diffs.length, onActivePathChange, updateActivePath]); +function selectionLineNumbers(range: SelectedLineRange) { + const start = Math.min(range.start, range.end); + const end = Math.max(range.start, range.end); + if (range.side === "deletions" || range.endSide === "deletions") { + return { oldLine: start, endOldLine: end, newLine: null, endNewLine: null }; + } + if (range.side === "additions" || range.endSide === "additions") { + return { newLine: start, endNewLine: end, oldLine: null, endOldLine: null }; + } + return { newLine: start, endNewLine: end, oldLine: null, endOldLine: null }; +} + +type DiffCardProps = { + entry: ParsedDiffEntry; + isSelected: boolean; + selectedRange: SelectedRange | null; + onLineSelectionEnd: (entry: ParsedDiffEntry, range: SelectedLineRange | null) => void; +}; + +const DiffCard = memo(function DiffCard({ + entry, + isSelected, + selectedRange, + onLineSelectionEnd, +}: DiffCardProps) { + const selectedLines = useMemo( + () => + selectedRange + ? { + start: selectedRange.start, + end: selectedRange.end, + side: selectedRange.side, + endSide: selectedRange.endSide, + } + : undefined, + [selectedRange], + ); + const diffOptions = useMemo( + () => ({ + diffStyle: "split" as const, + hunkSeparators: "line-info" as const, + enableLineSelection: true, + overflow: "scroll" as const, + unsafeCSS: DIFF_SCROLL_CSS, + onLineSelectionEnd: (range: SelectedLineRange | null) => + onLineSelectionEnd(entry, range), + disableFileHeader: true, + }), + [entry, onLineSelectionEnd], + ); + + return ( +
+
+ {entry.status} + {entry.path} +
+ {entry.diff.trim().length > 0 && entry.fileDiff ? ( +
+ +
+ ) : ( +
Diff unavailable.
+ )} +
+ ); +}); + +export function GitDiffViewer({ + diffs, + selectedPath, + isLoading, + error, + onLineReference, +}: GitDiffViewerProps) { + const [selectedRange, setSelectedRange] = useState(null); + const poolOptions = useMemo(() => ({ workerFactory }), []); + const highlighterOptions = useMemo( + () => ({ theme: { dark: "pierre-dark", light: "pierre-light" } }), + [], + ); + const parsedDiffs = useMemo( + () => + diffs.map((entry) => { + const patch = parsePatchFiles(entry.diff); + const fileDiff = patch[0]?.files[0]; + if (!fileDiff) { + return { ...entry, fileDiff: null, lineMaps: null }; + } + const normalizedName = normalizePatchName(fileDiff.name || entry.path); + const normalizedPrevName = fileDiff.prevName + ? normalizePatchName(fileDiff.prevName) + : undefined; + const normalized: FileDiffMetadata = { + ...fileDiff, + name: normalizedName, + prevName: normalizedPrevName, + }; + return { + ...entry, + fileDiff: normalized, + lineMaps: buildLineMaps(normalized.hunks), + }; + }), + [diffs], + ); useEffect(() => { if (!selectedRange) { @@ -177,110 +254,69 @@ export function GitDiffViewer({ } }, [diffs, selectedRange]); - const handleLineSelect = ( - entry: GitDiffViewerItem, - parsedLines: ParsedDiffLine[], - line: ParsedDiffLine, - index: number, - isRangeSelect: boolean, - ) => { - if (!isSelectableLine(line)) { - return; - } - const hasAnchor = selectedRange?.path === entry.path; - const anchor = isRangeSelect && hasAnchor ? selectedRange.anchor : index; - const start = isRangeSelect ? Math.min(anchor, index) : index; - const end = isRangeSelect ? Math.max(anchor, index) : index; - setSelectedRange({ path: entry.path, start, end, anchor }); - - const selectedLines = parsedLines - .slice(start, end + 1) - .filter(isSelectableLine); - if (selectedLines.length === 0) { - return; - } - - const typeSet = new Set(selectedLines.map((item) => item.type)); - const selectionType = typeSet.size === 1 ? selectedLines[0].type : "mixed"; - const firstOldLine = selectedLines.find((item) => item.oldLine !== null)?.oldLine ?? null; - const firstNewLine = selectedLines.find((item) => item.newLine !== null)?.newLine ?? null; - const lastOldLine = - [...selectedLines].reverse().find((item) => item.oldLine !== null)?.oldLine ?? - null; - const lastNewLine = - [...selectedLines].reverse().find((item) => item.newLine !== null)?.newLine ?? - null; - - onLineReference?.({ - path: entry.path, - type: selectionType, - oldLine: firstOldLine, - newLine: firstNewLine, - endOldLine: lastOldLine, - endNewLine: lastNewLine, - lines: selectedLines.map((item) => item.text), - }); - }; + const handleSelectionEnd = useCallback( + (entry: ParsedDiffEntry, range: SelectedLineRange | null) => { + if (!range || !entry.lineMaps) { + return; + } + const start = Math.min(range.start, range.end); + const end = Math.max(range.start, range.end); + setSelectedRange({ + path: entry.path, + start, + end, + anchor: start, + side: range.side, + endSide: range.endSide, + }); + const lines = collectSelectedLines(range, entry.lineMaps); + if (!lines.length) { + return; + } + const { oldLine, endOldLine, newLine, endNewLine } = selectionLineNumbers(range); + onLineReference?.({ + path: entry.path, + type: selectionTypeFromSide(range.side, range.endSide), + oldLine, + newLine, + endOldLine, + endNewLine, + lines, + }); + }, + [onLineReference], + ); return ( -
- {error &&
{error}
} - {!error && isLoading && diffs.length > 0 && ( -
Refreshing diff...
- )} - {!error && !isLoading && !diffs.length && ( -
No changes detected.
- )} - {!error && - diffs.map((entry) => { - const isSelected = entry.path === selectedPath; - const hasDiff = entry.diff.trim().length > 0; - const language = languageFromPath(entry.path); - const parsedLines = parseDiff(entry.diff); - const selectedRangeForEntry = - selectedRange?.path === entry.path - ? { start: selectedRange.start, end: selectedRange.end } - : null; - return ( -
{ - if (node) { - itemRefs.current.set(entry.path, node); - } else { - itemRefs.current.delete(entry.path); - } - }} - className={`diff-viewer-item ${isSelected ? "active" : ""}`} - > -
- {entry.status} - {entry.path} -
- {hasDiff ? ( -
- - handleLineSelect( - entry, - parsedLines, - line, - index, - "shiftKey" in event && event.shiftKey, - ) - } - selectedRange={selectedRangeForEntry} - /> -
- ) : ( -
Diff unavailable.
- )} -
- ); - })} -
+ +
+ {error &&
{error}
} + {!error && isLoading && diffs.length > 0 && ( +
Refreshing diff...
+ )} + {!error && !isLoading && !diffs.length && ( +
No changes detected.
+ )} + {!error && parsedDiffs.length > 0 && ( + parsedDiffs.map((entry) => { + const isSelected = entry.path === selectedPath; + const selectedRangeForEntry = + selectedRange?.path === entry.path ? selectedRange : null; + return ( + + ); + }) + )} +
+
); } diff --git a/src/features/layout/hooks/useLayoutNodes.tsx b/src/features/layout/hooks/useLayoutNodes.tsx index 18760f09e..7cc5041ce 100644 --- a/src/features/layout/hooks/useLayoutNodes.tsx +++ b/src/features/layout/hooks/useLayoutNodes.tsx @@ -153,7 +153,7 @@ type LayoutNodesOptions = { gitDiffLoading: boolean; gitDiffError: string | null; onDiffLineReference: (reference: DiffLineReference) => void; - onDiffActivePathChange: (path: string) => void; + onDiffActivePathChange?: (path: string) => void; onSend: (text: string, images: string[]) => void | Promise; onQueue: (text: string, images: string[]) => void | Promise; onStop: () => void; diff --git a/src/styles/base.css b/src/styles/base.css index 1658344f4..13f12b09a 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -214,6 +214,10 @@ body { transition: grid-template-columns 220ms ease; } +.app.ui-scale-default { + transform: none; +} + .drag-strip { position: absolute; top: 0; diff --git a/src/styles/diff-viewer.css b/src/styles/diff-viewer.css index 7aece2371..7f1671a54 100644 --- a/src/styles/diff-viewer.css +++ b/src/styles/diff-viewer.css @@ -7,6 +7,13 @@ flex: 1; min-height: 0; min-width: 0; + background: var(--surface-messages); +} + + +.main .diff-viewer { + margin-top: calc(-1 * var(--main-topbar-height)); + padding-top: calc(12px + var(--main-topbar-height)); } .diff-viewer-item { @@ -56,7 +63,21 @@ color: var(--text-quiet); font-family: "SFMono-Regular", "Menlo", "Monaco", monospace; max-width: 100%; - overflow-x: auto; + min-width: 0; + width: 100%; + overflow: hidden; + --diffs-font-family: "SFMono-Regular", "Menlo", "Monaco", monospace; + --diffs-font-size: 11px; + --diffs-line-height: 1.4; + --diffs-tab-size: 2; +} + + +.diff-viewer-output diffs-container { + display: block; + max-width: 100%; + min-width: 0; + width: 100%; } .diff-line { diff --git a/src/styles/main.css b/src/styles/main.css index 6679a463a..446704a0c 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -233,6 +233,7 @@ backdrop-filter: none; } + .main-topbar-left { display: flex; align-items: center; diff --git a/src/utils/diffsWorker.ts b/src/utils/diffsWorker.ts new file mode 100644 index 000000000..fe03489bc --- /dev/null +++ b/src/utils/diffsWorker.ts @@ -0,0 +1,5 @@ +import WorkerUrl from "@pierre/diffs/worker/worker.js?worker&url"; + +export function workerFactory(): Worker { + return new Worker(WorkerUrl, { type: "module" }); +} diff --git a/vite.config.ts b/vite.config.ts index 6bded1f6a..c62a7d169 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,6 +7,9 @@ const host = process.env.TAURI_DEV_HOST; // https://vite.dev/config/ export default defineConfig(async () => ({ plugins: [react()], + worker: { + format: "es", + }, // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // From 40d9b344478d3672f40ccb1ce24ef705b2f52367 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sat, 17 Jan 2026 20:50:00 +0100 Subject: [PATCH 2/4] style: drop active diff sidebar highlight --- src/features/git/components/GitDiffPanel.tsx | 2 +- src/features/git/components/GitDiffViewer.tsx | 34 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/features/git/components/GitDiffPanel.tsx b/src/features/git/components/GitDiffPanel.tsx index d4b18a39b..358e11744 100644 --- a/src/features/git/components/GitDiffPanel.tsx +++ b/src/features/git/components/GitDiffPanel.tsx @@ -277,7 +277,7 @@ export function GitDiffPanel({ return (
onSelectFile?.(file.path)} diff --git a/src/features/git/components/GitDiffViewer.tsx b/src/features/git/components/GitDiffViewer.tsx index 7e7f71eb0..6c618565e 100644 --- a/src/features/git/components/GitDiffViewer.tsx +++ b/src/features/git/components/GitDiffViewer.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useEffect, useMemo, useState } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FileDiff, WorkerPoolContextProvider } from "@pierre/diffs/react"; import type { FileDiffMetadata, @@ -212,6 +212,7 @@ export function GitDiffViewer({ error, onLineReference, }: GitDiffViewerProps) { + const containerRef = useRef(null); const [selectedRange, setSelectedRange] = useState(null); const poolOptions = useMemo(() => ({ workerFactory }), []); const highlighterOptions = useMemo( @@ -254,6 +255,35 @@ export function GitDiffViewer({ } }, [diffs, selectedRange]); + useEffect(() => { + if (!selectedPath) { + return; + } + const container = containerRef.current; + if (!container) { + return; + } + let target: HTMLElement | null = null; + const items = container.querySelectorAll("[data-diff-path]"); + for (const item of items) { + if (item.dataset.diffPath === selectedPath) { + target = item; + break; + } + } + if (!target) { + return; + } + const containerRect = container.getBoundingClientRect(); + const targetRect = target.getBoundingClientRect(); + const isVisible = + targetRect.top >= containerRect.top && + targetRect.bottom <= containerRect.bottom; + if (!isVisible) { + target.scrollIntoView({ block: "start" }); + } + }, [selectedPath, parsedDiffs]); + const handleSelectionEnd = useCallback( (entry: ParsedDiffEntry, range: SelectedLineRange | null) => { if (!range || !entry.lineMaps) { @@ -292,7 +322,7 @@ export function GitDiffViewer({ poolOptions={poolOptions} highlighterOptions={highlighterOptions} > -
+
{error &&
{error}
} {!error && isLoading && diffs.length > 0 && (
Refreshing diff...
From 4e583d6bf47ab854ba73de3605e4adc8b4a56e6c Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sat, 17 Jan 2026 21:04:37 +0100 Subject: [PATCH 3/4] fix: remove unused diff selection --- src/features/git/components/GitDiffPanel.tsx | 3 --- src/features/layout/hooks/useLayoutNodes.tsx | 1 - 2 files changed, 4 deletions(-) diff --git a/src/features/git/components/GitDiffPanel.tsx b/src/features/git/components/GitDiffPanel.tsx index 358e11744..5cbb61636 100644 --- a/src/features/git/components/GitDiffPanel.tsx +++ b/src/features/git/components/GitDiffPanel.tsx @@ -29,7 +29,6 @@ type GitDiffPanelProps = { issuesLoading?: boolean; issuesError?: string | null; gitRemoteUrl?: string | null; - selectedPath?: string | null; onSelectFile?: (path: string) => void; files: { path: string; @@ -106,7 +105,6 @@ export function GitDiffPanel({ logLoading = false, logTotal = 0, gitRemoteUrl = null, - selectedPath, onSelectFile, files, logEntries, @@ -271,7 +269,6 @@ export function GitDiffPanel({ {files.map((file) => { const { name, dir } = splitPath(file.path); const { base, extension } = splitNameAndExtension(name); - const isSelected = file.path === selectedPath; const statusSymbol = getStatusSymbol(file.status); const statusClass = getStatusClass(file.status); return ( diff --git a/src/features/layout/hooks/useLayoutNodes.tsx b/src/features/layout/hooks/useLayoutNodes.tsx index 7cc5041ce..b5e6b384a 100644 --- a/src/features/layout/hooks/useLayoutNodes.tsx +++ b/src/features/layout/hooks/useLayoutNodes.tsx @@ -429,7 +429,6 @@ export function useLayoutNodes(options: LayoutNodesOptions): LayoutNodesResult { logError={options.gitLogError} logLoading={options.gitLogLoading} files={options.gitStatus.files} - selectedPath={options.selectedDiffPath} onSelectFile={options.onSelectDiff} logEntries={options.gitLogEntries} logTotal={options.gitLogTotal} From 30fd24c0a72e370d2c11bd9de08b6d6ab98ae07a Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sat, 17 Jan 2026 21:13:07 +0100 Subject: [PATCH 4/4] fix: stop diff viewer auto-scroll --- src/App.tsx | 34 +--- src/features/git/components/GitDiffViewer.tsx | 156 ++---------------- src/features/layout/hooks/useLayoutNodes.tsx | 3 - src/types.ts | 10 -- 4 files changed, 15 insertions(+), 188 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 3c4a8cf9b..75580e199 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -72,7 +72,7 @@ import { useCopyThread } from "./features/threads/hooks/useCopyThread"; import { usePanelVisibility } from "./features/layout/hooks/usePanelVisibility"; import { useTerminalController } from "./features/terminal/hooks/useTerminalController"; import { playNotificationSound } from "./utils/notificationSounds"; -import type { AccessMode, DiffLineReference, QueuedMessage, WorkspaceInfo } from "./types"; +import type { AccessMode, QueuedMessage, WorkspaceInfo } from "./types"; function useWindowLabel() { const [label, setLabel] = useState("main"); @@ -627,37 +627,6 @@ function MainApp() { } } - function handleDiffLineReference(reference: DiffLineReference) { - const startLine = reference.newLine ?? reference.oldLine; - const endLine = - reference.endNewLine ?? reference.endOldLine ?? startLine ?? null; - const lineRange = - startLine && endLine && endLine !== startLine - ? `${startLine}-${endLine}` - : startLine - ? `${startLine}` - : null; - const lineLabel = lineRange - ? `${reference.path}:${lineRange}` - : reference.path; - const changeLabel = - reference.type === "add" - ? "added" - : reference.type === "del" - ? "removed" - : reference.type === "mixed" - ? "mixed" - : "context"; - const snippet = reference.lines.join("\n").trimEnd(); - const snippetBlock = snippet ? `\n\`\`\`\n${snippet}\n\`\`\`` : ""; - const label = reference.lines.length > 1 ? "Line range" : "Line reference"; - const text = `${label} (${changeLabel}): ${lineLabel}${snippetBlock}`; - setComposerInsert({ - id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - text, - createdAt: Date.now() - }); - } const handleOpenSettings = useCallback( (section?: SettingsSection) => { @@ -904,7 +873,6 @@ function MainApp() { gitDiffs, gitDiffLoading: isDiffLoading, gitDiffError: diffError, - onDiffLineReference: handleDiffLineReference, onSend: handleSend, onQueue: queueMessage, onStop: interruptTurn, diff --git a/src/features/git/components/GitDiffViewer.tsx b/src/features/git/components/GitDiffViewer.tsx index 6c618565e..8440ea13c 100644 --- a/src/features/git/components/GitDiffViewer.tsx +++ b/src/features/git/components/GitDiffViewer.tsx @@ -1,13 +1,10 @@ -import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { memo, useEffect, useMemo, useRef } from "react"; import { FileDiff, WorkerPoolContextProvider } from "@pierre/diffs/react"; import type { FileDiffMetadata, Hunk, - SelectedLineRange, - AnnotationSide, } from "@pierre/diffs"; import { parsePatchFiles } from "@pierre/diffs"; -import type { DiffLineReference } from "../../../types"; import { workerFactory } from "../../../utils/diffsWorker"; type GitDiffViewerItem = { @@ -21,19 +18,9 @@ type GitDiffViewerProps = { selectedPath: string | null; isLoading: boolean; error: string | null; - onLineReference?: (reference: DiffLineReference) => void; onActivePathChange?: (path: string) => void; }; -type SelectedRange = { - path: string; - start: number; - end: number; - anchor: number; - side?: AnnotationSide; - endSide?: AnnotationSide; -}; - type LineMaps = { oldLines: Map; newLines: Map; @@ -93,90 +80,24 @@ function buildLineMaps(hunks: Hunk[]): LineMaps { return { oldLines, newLines }; } -function selectionTypeFromSide(side?: AnnotationSide, endSide?: AnnotationSide) { - if (side && endSide && side !== endSide) { - return "mixed"; - } - if (side === "additions" || endSide === "additions") { - return "add"; - } - if (side === "deletions" || endSide === "deletions") { - return "del"; - } - return "context"; -} - -function collectSelectedLines( - range: SelectedLineRange, - lineMaps: LineMaps, -) { - const start = Math.min(range.start, range.end); - const end = Math.max(range.start, range.end); - const useNew = range.side === "additions" || range.endSide === "additions"; - const useOld = range.side === "deletions" || range.endSide === "deletions"; - const lines: string[] = []; - for (let lineNumber = start; lineNumber <= end; lineNumber += 1) { - const line = useNew - ? lineMaps.newLines.get(lineNumber) - : useOld - ? lineMaps.oldLines.get(lineNumber) - : lineMaps.newLines.get(lineNumber) ?? lineMaps.oldLines.get(lineNumber); - if (line !== undefined) { - lines.push(line); - } - } - return lines; -} - -function selectionLineNumbers(range: SelectedLineRange) { - const start = Math.min(range.start, range.end); - const end = Math.max(range.start, range.end); - if (range.side === "deletions" || range.endSide === "deletions") { - return { oldLine: start, endOldLine: end, newLine: null, endNewLine: null }; - } - if (range.side === "additions" || range.endSide === "additions") { - return { newLine: start, endNewLine: end, oldLine: null, endOldLine: null }; - } - return { newLine: start, endNewLine: end, oldLine: null, endOldLine: null }; -} - type DiffCardProps = { entry: ParsedDiffEntry; isSelected: boolean; - selectedRange: SelectedRange | null; - onLineSelectionEnd: (entry: ParsedDiffEntry, range: SelectedLineRange | null) => void; }; const DiffCard = memo(function DiffCard({ entry, isSelected, - selectedRange, - onLineSelectionEnd, }: DiffCardProps) { - const selectedLines = useMemo( - () => - selectedRange - ? { - start: selectedRange.start, - end: selectedRange.end, - side: selectedRange.side, - endSide: selectedRange.endSide, - } - : undefined, - [selectedRange], - ); const diffOptions = useMemo( () => ({ diffStyle: "split" as const, hunkSeparators: "line-info" as const, - enableLineSelection: true, overflow: "scroll" as const, unsafeCSS: DIFF_SCROLL_CSS, - onLineSelectionEnd: (range: SelectedLineRange | null) => - onLineSelectionEnd(entry, range), disableFileHeader: true, }), - [entry, onLineSelectionEnd], + [], ); return ( @@ -192,7 +113,6 @@ const DiffCard = memo(function DiffCard({
(null); - const [selectedRange, setSelectedRange] = useState(null); + const lastScrolledPathRef = useRef(null); const poolOptions = useMemo(() => ({ workerFactory }), []); const highlighterOptions = useMemo( () => ({ theme: { dark: "pierre-dark", light: "pierre-light" } }), @@ -246,17 +165,10 @@ export function GitDiffViewer({ ); useEffect(() => { - if (!selectedRange) { + if (!selectedPath) { return; } - const stillExists = diffs.some((entry) => entry.path === selectedRange.path); - if (!stillExists) { - setSelectedRange(null); - } - }, [diffs, selectedRange]); - - useEffect(() => { - if (!selectedPath) { + if (lastScrolledPathRef.current === selectedPath) { return; } const container = containerRef.current; @@ -282,41 +194,9 @@ export function GitDiffViewer({ if (!isVisible) { target.scrollIntoView({ block: "start" }); } + lastScrolledPathRef.current = selectedPath; }, [selectedPath, parsedDiffs]); - const handleSelectionEnd = useCallback( - (entry: ParsedDiffEntry, range: SelectedLineRange | null) => { - if (!range || !entry.lineMaps) { - return; - } - const start = Math.min(range.start, range.end); - const end = Math.max(range.start, range.end); - setSelectedRange({ - path: entry.path, - start, - end, - anchor: start, - side: range.side, - endSide: range.endSide, - }); - const lines = collectSelectedLines(range, entry.lineMaps); - if (!lines.length) { - return; - } - const { oldLine, endOldLine, newLine, endNewLine } = selectionLineNumbers(range); - onLineReference?.({ - path: entry.path, - type: selectionTypeFromSide(range.side, range.endSide), - oldLine, - newLine, - endOldLine, - endNewLine, - lines, - }); - }, - [onLineReference], - ); - return ( No changes detected.
)} - {!error && parsedDiffs.length > 0 && ( - parsedDiffs.map((entry) => { - const isSelected = entry.path === selectedPath; - const selectedRangeForEntry = - selectedRange?.path === entry.path ? selectedRange : null; - return ( - - ); - }) - )} + {!error && parsedDiffs.length > 0 && + parsedDiffs.map((entry) => ( + + ))}
); diff --git a/src/features/layout/hooks/useLayoutNodes.tsx b/src/features/layout/hooks/useLayoutNodes.tsx index b5e6b384a..f44d4afc3 100644 --- a/src/features/layout/hooks/useLayoutNodes.tsx +++ b/src/features/layout/hooks/useLayoutNodes.tsx @@ -23,7 +23,6 @@ import type { ConversationItem, CustomPromptOption, DebugEntry, - DiffLineReference, DictationSessionState, DictationTranscript, GitFileStatus, @@ -152,7 +151,6 @@ type LayoutNodesOptions = { gitDiffs: GitDiffViewerItem[]; gitDiffLoading: boolean; gitDiffError: string | null; - onDiffLineReference: (reference: DiffLineReference) => void; onDiffActivePathChange?: (path: string) => void; onSend: (text: string, images: string[]) => void | Promise; onQueue: (text: string, images: string[]) => void | Promise; @@ -452,7 +450,6 @@ export function useLayoutNodes(options: LayoutNodesOptions): LayoutNodesResult { selectedPath={options.selectedDiffPath} isLoading={options.gitDiffLoading} error={options.gitDiffError} - onLineReference={options.onDiffLineReference} onActivePathChange={options.onDiffActivePathChange} /> ); diff --git a/src/types.ts b/src/types.ts index 51807c7da..642c86b8c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -105,16 +105,6 @@ export type GitFileDiff = { diff: string; }; -export type DiffLineReference = { - path: string; - type: "add" | "del" | "context" | "mixed"; - oldLine: number | null; - newLine: number | null; - endOldLine: number | null; - endNewLine: number | null; - lines: string[]; -}; - export type GitLogEntry = { sha: string; summary: string;