perf(diff): Pierre diff optimizations (#419)

* perf: implement virtualized rendering for diff viewer

- Enables efficient rendering of large diffs by only rendering visible content
- Uses shared virtualizer cache to optimize memory across multiple diff viewers
- Configures virtual scrolling with 24px line height for consistent layout

* fix: improve diff viewer line selection and annotation rendering

* fix: normalize line ranges to prevent selection bugs

* chore: upgrade @opencode-ai/sdk to v1.1.65
This commit is contained in:
Bohdan Triapitsyn
2026-02-13 19:05:31 +02:00
committed by GitHub
parent 2f09d375d3
commit 523eafdf65
8 changed files with 301 additions and 59 deletions
+11 -11
View File
@@ -30,7 +30,7 @@
"@ibm/plex": "^6.4.1", "@ibm/plex": "^6.4.1",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@octokit/rest": "^22.0.1", "@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.1.53", "@opencode-ai/sdk": "^1.1.65",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -95,7 +95,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@openchamber/desktop", "name": "@openchamber/desktop",
"version": "1.6.6", "version": "1.6.8",
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2",
"@types/node": "^24.3.1", "@types/node": "^24.3.1",
@@ -104,7 +104,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@openchamber/ui", "name": "@openchamber/ui",
"version": "1.6.6", "version": "1.6.8",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.20.0", "@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.1", "@codemirror/commands": "^6.10.1",
@@ -132,8 +132,8 @@
"@fontsource/ibm-plex-sans": "^5.1.1", "@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1", "@ibm/plex": "^6.4.1",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "^1.1.53", "@opencode-ai/sdk": "^1.1.65",
"@pierre/diffs": "^1.0.5", "@pierre/diffs": "1.1.0-beta.13",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -198,10 +198,10 @@
}, },
"packages/vscode": { "packages/vscode": {
"name": "openchamber", "name": "openchamber",
"version": "1.6.6", "version": "1.6.8",
"dependencies": { "dependencies": {
"@openchamber/ui": "workspace:*", "@openchamber/ui": "workspace:*",
"@opencode-ai/sdk": "^1.1.53", "@opencode-ai/sdk": "^1.1.65",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"jsonc-parser": "^3.3.1", "jsonc-parser": "^3.3.1",
"react": "^19.1.1", "react": "^19.1.1",
@@ -221,7 +221,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@openchamber/web", "name": "@openchamber/web",
"version": "1.6.6", "version": "1.6.8",
"bin": { "bin": {
"openchamber": "./bin/cli.js", "openchamber": "./bin/cli.js",
}, },
@@ -230,7 +230,7 @@
"@fontsource/ibm-plex-sans": "^5.1.1", "@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1", "@ibm/plex": "^6.4.1",
"@octokit/rest": "^22.0.1", "@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.1.53", "@opencode-ai/sdk": "^1.1.65",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -843,9 +843,9 @@
"@openchamber/web": ["@openchamber/web@workspace:packages/web"], "@openchamber/web": ["@openchamber/web@workspace:packages/web"],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.53", "", {}, "sha512-RUIVnPOP1CyyU32FrOOYuE7Ge51lOBuhaFp2NSX98ncApT7ffoNetmwzqrhOiJQgZB1KrbCHLYOCK6AZfacxag=="], "@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.65", "", {}, "sha512-XlpgQJQ5WwO4tYgyyHoTT0NAB5/1StXonabVUAYVpW0JdtbwWFSdFEaLkWx6CU7MNW6ELP+SMC4n6wWO6zRW8Q=="],
"@pierre/diffs": ["@pierre/diffs@1.0.10", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "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" } }, "sha512-ahkpfS30NfaB+PBxnf0/Mc20ySBRTQmM28a7Ojpd0UZixmTyhGhJfBFjvmhX8dSzR22lB3h3OIMMxpB4yYTIOQ=="], "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "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" } }, "sha512-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
+1 -1
View File
@@ -83,7 +83,7 @@
"@ibm/plex": "^6.4.1", "@ibm/plex": "^6.4.1",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@octokit/rest": "^22.0.1", "@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.1.53", "@opencode-ai/sdk": "^1.1.65",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
+2 -2
View File
@@ -37,8 +37,8 @@
"@fontsource/ibm-plex-sans": "^5.1.1", "@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1", "@ibm/plex": "^6.4.1",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "^1.1.53", "@opencode-ai/sdk": "^1.1.65",
"@pierre/diffs": "^1.0.5", "@pierre/diffs": "1.1.0-beta.13",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -38,24 +38,62 @@ export function InlineCommentInput({
} }
}, [lineRange]); }, [lineRange]);
const normalizeRange = (range?: { start: number; end: number; side?: 'additions' | 'deletions' }) => {
if (!range) return undefined;
const start = Math.min(range.start, range.end);
const end = Math.max(range.start, range.end);
return { ...range, start, end };
};
const displayRange = normalizeRange(lineRange);
// Focus on mount (desktop only) or when becoming visible // Focus on mount (desktop only) or when becoming visible
useEffect(() => { useEffect(() => {
if (!isMobile && textareaRef.current) { const textarea = textareaRef.current;
textareaRef.current.focus(); if (!textarea) return;
// Move cursor to end
const len = textareaRef.current.value.length; const scrollContainer = textarea.closest('.overlay-scrollbar-container') as HTMLElement | null;
textareaRef.current.setSelectionRange(len, len); const prevScrollTop = scrollContainer?.scrollTop ?? window.scrollY;
} else if (isMobile && textareaRef.current) { const prevScrollLeft = scrollContainer?.scrollLeft ?? window.scrollX;
// Scroll into view on mobile
textareaRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); if (isMobile) {
textarea.scrollIntoView({ behavior: 'auto', block: 'nearest' });
try {
textarea.focus({ preventScroll: true });
} catch {
textarea.focus();
}
return;
} }
try {
textarea.focus({ preventScroll: true });
} catch {
textarea.focus();
}
const len = textarea.value.length;
try {
textarea.setSelectionRange(len, len);
} catch (err) {
void err;
}
requestAnimationFrame(() => {
if (scrollContainer) {
scrollContainer.scrollTop = prevScrollTop;
scrollContainer.scrollLeft = prevScrollLeft;
} else {
window.scrollTo({ top: prevScrollTop, left: prevScrollLeft });
}
});
}, [isMobile]); }, [isMobile]);
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
if (text.trim()) { if (text.trim()) {
onSave(text, stableRangeRef.current); onSave(text, normalizeRange(stableRangeRef.current));
} }
} else if (e.key === 'Escape') { } else if (e.key === 'Escape') {
e.preventDefault(); e.preventDefault();
@@ -67,7 +105,7 @@ export function InlineCommentInput({
// Stop propagation to prevent parent selection clearing before save // Stop propagation to prevent parent selection clearing before save
e.stopPropagation(); e.stopPropagation();
if (text.trim()) { if (text.trim()) {
onSave(text, stableRangeRef.current); onSave(text, normalizeRange(stableRangeRef.current));
} }
}; };
@@ -90,7 +128,7 @@ export function InlineCommentInput({
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground mb-2"> <div className="flex items-center gap-2 text-xs font-medium text-muted-foreground mb-2">
{fileLabel && <span className="truncate max-w-[200px]">{fileLabel}</span>} {fileLabel && <span className="truncate max-w-[200px]">{fileLabel}</span>}
{fileLabel && lineRange && <span>•</span>} {fileLabel && lineRange && <span>•</span>}
{lineRange && <span>Lines {lineRange.start}-{lineRange.end}</span>} {displayRange && <span>Lines {displayRange.start}-{displayRange.end}</span>}
</div> </div>
)} )}
@@ -1082,6 +1082,8 @@ export const DiffView: React.FC = () => {
outerClassName="flex-1 min-h-0 h-full" outerClassName="flex-1 min-h-0 h-full"
className="pr-2" className="pr-2"
disableHorizontal disableHorizontal
data-diff-virtual-root
data-diff-virtual-content
> >
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{changedFiles.map((file, index) => ( {changedFiles.map((file, index) => (
@@ -1144,7 +1146,7 @@ export const DiffView: React.FC = () => {
} }
return ( return (
<div className="flex flex-1 min-h-0 overflow-hidden px-3 py-3 relative"> <div className="flex flex-1 min-h-0 overflow-hidden px-3 py-3 relative" data-diff-virtual-root data-diff-virtual-content>
{renderSelectedDiffViewer()} {renderSelectedDiffViewer()}
{isCurrentFileLoading && !hasCurrentDiff && ( {isCurrentFileLoading && !hasCurrentDiff && (
<div className="absolute inset-0 flex items-center justify-center gap-2 text-sm text-muted-foreground"> <div className="absolute inset-0 flex items-center justify-center gap-2 text-sm text-muted-foreground">
@@ -1,6 +1,16 @@
import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react'; import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { FileDiff as PierreFileDiff, type FileContents, type FileDiffOptions, type SelectedLineRange, type DiffLineAnnotation, type AnnotationSide } from '@pierre/diffs'; import {
FileDiff as PierreFileDiff,
VirtualizedFileDiff,
Virtualizer,
type FileContents,
type FileDiffOptions,
type SelectedLineRange,
type DiffLineAnnotation,
type AnnotationSide,
type VirtualFileMetrics,
} from '@pierre/diffs';
import { InlineCommentCard, InlineCommentInput } from '@/components/comments'; import { InlineCommentCard, InlineCommentInput } from '@/components/comments';
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem'; import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
@@ -120,8 +130,10 @@ const extractSelectedCode = (original: string, modified: string, range: Selected
const lines = content.split('\n'); const lines = content.split('\n');
// Ensure bounds // Ensure bounds
const startLine = Math.max(1, range.start); const from = Math.min(range.start, range.end);
const endLine = Math.min(lines.length, range.end); const to = Math.max(range.start, range.end);
const startLine = Math.max(1, from);
const endLine = Math.min(lines.length, to);
if (startLine > endLine) return ''; if (startLine > endLine) return '';
@@ -138,6 +150,82 @@ type AnnotationData =
| { type: 'saved' | 'edit'; draft: InlineCommentDraft } | { type: 'saved' | 'edit'; draft: InlineCommentDraft }
| { type: 'new'; selection: SelectedLineRange }; | { type: 'new'; selection: SelectedLineRange };
type SharedVirtualizer = {
virtualizer: Virtualizer;
release: () => void;
};
type VirtualizerTarget = {
key: Document | HTMLElement;
root: Document | HTMLElement;
content: HTMLElement | undefined;
};
type VirtualizerEntry = {
virtualizer: Virtualizer;
refs: number;
};
const virtualizerCache = new WeakMap<Document | HTMLElement, VirtualizerEntry>();
const VIRTUAL_METRICS: Partial<VirtualFileMetrics> = {
lineHeight: 24,
hunkSeparatorHeight: 24,
fileGap: 0,
};
function resolveVirtualizerTarget(container: HTMLElement): VirtualizerTarget {
const root = container.closest('[data-diff-virtual-root]');
if (root instanceof HTMLElement) {
const content = root.querySelector('[data-diff-virtual-content]');
return {
key: root,
root,
content: content instanceof HTMLElement ? content : undefined,
};
}
return {
key: document,
root: document,
content: undefined,
};
}
function acquireSharedVirtualizer(container: HTMLElement): SharedVirtualizer | null {
if (typeof document === 'undefined') return null;
const target = resolveVirtualizerTarget(container);
let entry = virtualizerCache.get(target.key);
if (!entry) {
const virtualizer = new Virtualizer();
virtualizer.setup(target.root, target.content);
entry = { virtualizer, refs: 0 };
virtualizerCache.set(target.key, entry);
}
entry.refs += 1;
let released = false;
return {
virtualizer: entry.virtualizer,
release: () => {
if (released) return;
released = true;
const current = virtualizerCache.get(target.key);
if (!current) return;
current.refs -= 1;
if (current.refs > 0) return;
current.virtualizer.cleanUp();
virtualizerCache.delete(target.key);
},
};
}
export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
original, original,
modified, modified,
@@ -161,46 +249,54 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
const removeDraft = useInlineCommentDraftStore((state) => state.removeDraft); const removeDraft = useInlineCommentDraftStore((state) => state.removeDraft);
const allDrafts = useInlineCommentDraftStore((state) => state.drafts); const allDrafts = useInlineCommentDraftStore((state) => state.drafts);
const currentSessionId = useSessionStore((state) => state.currentSessionId); const currentSessionId = useSessionStore((state) => state.currentSessionId);
const newSessionDraftOpen = useSessionStore((state) => state.newSessionDraft?.open);
const getSessionKey = useCallback(() => { const getSessionKey = useCallback(() => {
return currentSessionId ?? (newSessionDraftOpen ? 'draft' : null); return currentSessionId ?? 'draft';
}, [currentSessionId, newSessionDraftOpen]); }, [currentSessionId]);
const [selection, setSelection] = useState<SelectedLineRange | null>(null); const [selection, setSelection] = useState<SelectedLineRange | null>(null);
const [commentText, setCommentText] = useState(''); const [commentText, setCommentText] = useState('');
const [editingDraftId, setEditingDraftId] = useState<string | null>(null); const [editingDraftId, setEditingDraftId] = useState<string | null>(null);
const selectionRef = useRef<SelectedLineRange | null>(null);
const editingDraftIdRef = useRef<string | null>(null);
// Use a ref to track if we're currently applying a selection programmatically // Use a ref to track if we're currently applying a selection programmatically
// to avoid loop with onLineSelected callback // to avoid loop with onLineSelected callback
const isApplyingSelectionRef = useRef(false); const isApplyingSelectionRef = useRef(false);
const lastAppliedSelectionRef = useRef<SelectedLineRange | null>(null); const lastAppliedSelectionRef = useRef<SelectedLineRange | null>(null);
useEffect(() => {
selectionRef.current = selection;
}, [selection]);
useEffect(() => {
editingDraftIdRef.current = editingDraftId;
}, [editingDraftId]);
const handleSelectionChange = useCallback((range: SelectedLineRange | null) => { const handleSelectionChange = useCallback((range: SelectedLineRange | null) => {
// Ignore callbacks while we're programmatically applying selection // Ignore callbacks while we're programmatically applying selection
if (isApplyingSelectionRef.current) { if (isApplyingSelectionRef.current) {
return; return;
} }
const prevSelection = selectionRef.current;
// Mobile tap-to-extend: if selection exists and new tap is on same side, extend range // Mobile tap-to-extend: if selection exists and new tap is on same side, extend range
if (isMobile && selection && range && range.side === selection.side) { if (isMobile && prevSelection && range && range.side === prevSelection.side) {
const start = Math.min(selection.start, range.start); const start = Math.min(prevSelection.start, range.start);
const end = Math.max(selection.end, range.end); const end = Math.max(prevSelection.end, range.end);
setSelection({ ...range, start, end }); setSelection({ ...range, start, end });
return; } else {
setSelection(range);
} }
setSelection(range);
// Clear editing state when selection changes user-driven // Clear editing state when selection changes user-driven
if (range) { if (range) {
// Don't clear if we're just updating the selection for the same draft? if (!editingDraftIdRef.current) {
// For now, simple behavior: new selection = new comment flow
if (!editingDraftId) {
setCommentText(''); setCommentText('');
} }
} }
}, [editingDraftId, isMobile, selection]); }, [isMobile]);
const handleCancelComment = useCallback(() => { const handleCancelComment = useCallback(() => {
setCommentText(''); setCommentText('');
@@ -258,6 +354,14 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
const targetRange = rangeOverride ?? selection; const targetRange = rangeOverride ?? selection;
if (!targetRange || !textToSave.trim()) return; if (!targetRange || !textToSave.trim()) return;
const normalizedStart = Math.min(targetRange.start, targetRange.end);
const normalizedEnd = Math.max(targetRange.start, targetRange.end);
const normalizedRange: SelectedLineRange = {
...targetRange,
start: normalizedStart,
end: normalizedEnd,
};
const sessionKey = getSessionKey(); const sessionKey = getSessionKey();
if (!sessionKey) { if (!sessionKey) {
toast.error('Select a session to save comment'); toast.error('Select a session to save comment');
@@ -267,16 +371,16 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
// Pierre selection range: { start, end, side } // Pierre selection range: { start, end, side }
// Store needs { startLine, endLine, side: 'original'|'modified' } // Store needs { startLine, endLine, side: 'original'|'modified' }
// Pierre side: 'additions' (right) | 'deletions' (left) // Pierre side: 'additions' (right) | 'deletions' (left)
const storeSide = targetRange.side === 'deletions' ? 'original' : 'modified'; const storeSide = normalizedRange.side === 'deletions' ? 'original' : 'modified';
// Use deterministic code extraction instead of instance.getSelectedText() // Use deterministic code extraction instead of instance.getSelectedText()
const selectedText = extractSelectedCode(original, modified, targetRange); const selectedText = extractSelectedCode(original, modified, normalizedRange);
if (editingDraftId) { if (editingDraftId) {
updateDraft(sessionKey, editingDraftId, { updateDraft(sessionKey, editingDraftId, {
fileLabel: fileName || 'unknown', fileLabel: fileName || 'unknown',
startLine: targetRange.start, startLine: normalizedRange.start,
endLine: targetRange.end, endLine: normalizedRange.end,
side: storeSide, side: storeSide,
code: selectedText, code: selectedText,
language: language, language: language,
@@ -287,8 +391,8 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
sessionKey, sessionKey,
source: 'diff', source: 'diff',
fileLabel: fileName || 'unknown', fileLabel: fileName || 'unknown',
startLine: targetRange.start, startLine: normalizedRange.start,
endLine: targetRange.end, endLine: normalizedRange.end,
side: storeSide, side: storeSide,
code: selectedText, code: selectedText,
language: language, language: language,
@@ -325,6 +429,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
const diffRootRef = useRef<HTMLDivElement | null>(null); const diffRootRef = useRef<HTMLDivElement | null>(null);
const diffContainerRef = useRef<HTMLDivElement | null>(null); const diffContainerRef = useRef<HTMLDivElement | null>(null);
const diffInstanceRef = useRef<PierreFileDiff<unknown> | null>(null); const diffInstanceRef = useRef<PierreFileDiff<unknown> | null>(null);
const sharedVirtualizerRef = useRef<SharedVirtualizer | null>(null);
const [, forceUpdate] = React.useReducer((x) => x + 1, 0); const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
const workerPool = useWorkerPool(); const workerPool = useWorkerPool();
@@ -417,6 +522,9 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
hunkSeparators: 'line-info' as const, hunkSeparators: 'line-info' as const,
// Perf: disable intra-line diff (word-level) globally. // Perf: disable intra-line diff (word-level) globally.
lineDiffType: 'none' as const, lineDiffType: 'none' as const,
maxLineDiffLength: 1000,
maxLineLengthForHighlighting: 1000,
expansionLineCount: 20,
overflow: wrapLines ? ('wrap' as const) : ('scroll' as const), overflow: wrapLines ? ('wrap' as const) : ('scroll' as const),
disableFileHeader: true, disableFileHeader: true,
enableLineSelection: true, enableLineSelection: true,
@@ -469,7 +577,12 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
return anns; return anns;
}, [allDrafts, getSessionKey, fileName, editingDraftId, selection]); }, [allDrafts, getSessionKey, fileName, editingDraftId, selection]);
// Imperative render (like upstream OpenCode): avoids `parseDiffFromFile` on main thread. const lineAnnotationsRef = useRef(lineAnnotations);
useEffect(() => {
lineAnnotationsRef.current = lineAnnotations;
}, [lineAnnotations]);
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
@@ -480,9 +593,21 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
// Dispose previous instance // Dispose previous instance
diffInstanceRef.current?.cleanUp(); diffInstanceRef.current?.cleanUp();
diffInstanceRef.current = null; diffInstanceRef.current = null;
sharedVirtualizerRef.current?.release();
sharedVirtualizerRef.current = null;
container.innerHTML = ''; container.innerHTML = '';
const instance = new PierreFileDiff(options as unknown as FileDiffOptions<unknown>, workerPool); const sharedVirtualizer = acquireSharedVirtualizer(container);
sharedVirtualizerRef.current = sharedVirtualizer;
const instance = sharedVirtualizer
? new VirtualizedFileDiff(
options as unknown as FileDiffOptions<unknown>,
sharedVirtualizer.virtualizer,
VIRTUAL_METRICS,
workerPool,
)
: new PierreFileDiff(options as unknown as FileDiffOptions<unknown>, workerPool);
diffInstanceRef.current = instance; diffInstanceRef.current = instance;
lastAppliedSelectionRef.current = null; lastAppliedSelectionRef.current = null;
@@ -502,7 +627,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
instance.render({ instance.render({
oldFile, oldFile,
newFile, newFile,
lineAnnotations, lineAnnotations: lineAnnotationsRef.current,
containerWrapper: container, containerWrapper: container,
}); });
@@ -514,9 +639,29 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
if (diffInstanceRef.current === instance) { if (diffInstanceRef.current === instance) {
diffInstanceRef.current = null; diffInstanceRef.current = null;
} }
sharedVirtualizer?.release();
if (sharedVirtualizer && sharedVirtualizerRef.current === sharedVirtualizer) {
sharedVirtualizerRef.current = null;
}
container.innerHTML = ''; container.innerHTML = '';
}; };
}, [diffThemeKey, fileName, language, modified, options, original, workerPool, lineAnnotations]); }, [diffThemeKey, fileName, language, modified, options, original, workerPool]);
useEffect(() => {
const instance = diffInstanceRef.current;
if (!instance) return;
instance.setLineAnnotations(lineAnnotations);
requestAnimationFrame(() => {
if (diffInstanceRef.current !== instance) return;
try {
instance.rerender();
} catch (err) {
void err;
}
forceUpdate();
});
}, [lineAnnotations]);
useEffect(() => { useEffect(() => {
const instance = diffInstanceRef.current; const instance = diffInstanceRef.current;
@@ -545,6 +690,64 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
} }
}, [selection]); }, [selection]);
useEffect(() => {
const container = diffContainerRef.current;
if (!container) return;
let rafId: number | null = null;
let cleanup = () => {};
const setup = () => {
const host = container.querySelector('diffs-container');
const shadowRoot = host?.shadowRoot;
if (!shadowRoot) {
rafId = requestAnimationFrame(setup);
return;
}
const onClickCapture = (event: Event) => {
if (!(event instanceof MouseEvent) || event.button !== 0) return;
if (!(event.target instanceof Element)) return;
const numberCell = event.target.closest('[data-column-number]');
if (!(numberCell instanceof HTMLElement)) return;
const lineRaw = numberCell.getAttribute('data-column-number');
const lineNumber = lineRaw ? parseInt(lineRaw, 10) : NaN;
if (Number.isNaN(lineNumber)) return;
const lineType =
numberCell.closest('[data-line-type]')?.getAttribute('data-line-type')
?? numberCell.getAttribute('data-line-type');
const side: AnnotationSide = lineType === 'change-deletion' ? 'deletions' : 'additions';
handleSelectionChange({
start: lineNumber,
end: lineNumber,
side,
});
event.preventDefault();
event.stopPropagation();
};
shadowRoot.addEventListener('click', onClickCapture, true);
cleanup = () => {
shadowRoot.removeEventListener('click', onClickCapture, true);
};
};
setup();
return () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
cleanup();
};
}, [diffThemeKey, fileName, handleSelectionChange]);
// MutationObserver to trigger re-renders when annotation DOM nodes are added/removed // MutationObserver to trigger re-renders when annotation DOM nodes are added/removed
useEffect(() => { useEffect(() => {
const container = diffContainerRef.current; const container = diffContainerRef.current;
@@ -669,12 +872,13 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
if (layout === 'fill') { if (layout === 'fill') {
return ( return (
<div className={cn("flex flex-col relative", "size-full")}> <div className={cn("flex flex-col relative", "size-full")} data-diff-virtual-root>
<div className="flex-1 relative min-h-0"> <div className="flex-1 relative min-h-0">
<ScrollableOverlay <ScrollableOverlay
outerClassName="pierre-diff-wrapper size-full" outerClassName="pierre-diff-wrapper size-full"
disableHorizontal={false} disableHorizontal={false}
fillContainer={true} fillContainer={true}
data-diff-virtual-content
> >
<div ref={diffRootRef} className="size-full relative"> <div ref={diffRootRef} className="size-full relative">
<div ref={diffContainerRef} className="size-full" /> <div ref={diffContainerRef} className="size-full" />
@@ -696,5 +900,3 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
</div> </div>
); );
}; };
+1 -1
View File
@@ -229,7 +229,7 @@
}, },
"dependencies": { "dependencies": {
"@openchamber/ui": "workspace:*", "@openchamber/ui": "workspace:*",
"@opencode-ai/sdk": "^1.1.53", "@opencode-ai/sdk": "^1.1.65",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"jsonc-parser": "^3.3.1", "jsonc-parser": "^3.3.1",
"react": "^19.1.1", "react": "^19.1.1",
+1 -1
View File
@@ -26,7 +26,7 @@
"@fontsource/ibm-plex-sans": "^5.1.1", "@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1", "@ibm/plex": "^6.4.1",
"@octokit/rest": "^22.0.1", "@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.1.53", "@opencode-ai/sdk": "^1.1.65",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",