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:
committed by
GitHub
parent
2f09d375d3
commit
523eafdf65
@@ -37,8 +37,8 @@
|
||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||
"@ibm/plex": "^6.4.1",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@opencode-ai/sdk": "^1.1.53",
|
||||
"@pierre/diffs": "^1.0.5",
|
||||
"@opencode-ai/sdk": "^1.1.65",
|
||||
"@pierre/diffs": "1.1.0-beta.13",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
|
||||
@@ -38,24 +38,62 @@ export function InlineCommentInput({
|
||||
}
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
if (!isMobile && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
// Move cursor to end
|
||||
const len = textareaRef.current.value.length;
|
||||
textareaRef.current.setSelectionRange(len, len);
|
||||
} else if (isMobile && textareaRef.current) {
|
||||
// Scroll into view on mobile
|
||||
textareaRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
const scrollContainer = textarea.closest('.overlay-scrollbar-container') as HTMLElement | null;
|
||||
const prevScrollTop = scrollContainer?.scrollTop ?? window.scrollY;
|
||||
const prevScrollLeft = scrollContainer?.scrollLeft ?? window.scrollX;
|
||||
|
||||
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]);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (text.trim()) {
|
||||
onSave(text, stableRangeRef.current);
|
||||
onSave(text, normalizeRange(stableRangeRef.current));
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
@@ -67,7 +105,7 @@ export function InlineCommentInput({
|
||||
// Stop propagation to prevent parent selection clearing before save
|
||||
e.stopPropagation();
|
||||
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">
|
||||
{fileLabel && <span className="truncate max-w-[200px]">{fileLabel}</span>}
|
||||
{fileLabel && lineRange && <span>•</span>}
|
||||
{lineRange && <span>Lines {lineRange.start}-{lineRange.end}</span>}
|
||||
{displayRange && <span>Lines {displayRange.start}-{displayRange.end}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1082,6 +1082,8 @@ export const DiffView: React.FC = () => {
|
||||
outerClassName="flex-1 min-h-0 h-full"
|
||||
className="pr-2"
|
||||
disableHorizontal
|
||||
data-diff-virtual-root
|
||||
data-diff-virtual-content
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
{changedFiles.map((file, index) => (
|
||||
@@ -1144,7 +1146,7 @@ export const DiffView: React.FC = () => {
|
||||
}
|
||||
|
||||
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()}
|
||||
{isCurrentFileLoading && !hasCurrentDiff && (
|
||||
<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 { 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 { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
||||
@@ -120,8 +130,10 @@ const extractSelectedCode = (original: string, modified: string, range: Selected
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Ensure bounds
|
||||
const startLine = Math.max(1, range.start);
|
||||
const endLine = Math.min(lines.length, range.end);
|
||||
const from = Math.min(range.start, 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 '';
|
||||
|
||||
@@ -138,6 +150,82 @@ type AnnotationData =
|
||||
| { type: 'saved' | 'edit'; draft: InlineCommentDraft }
|
||||
| { 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> = ({
|
||||
original,
|
||||
modified,
|
||||
@@ -161,46 +249,54 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
const removeDraft = useInlineCommentDraftStore((state) => state.removeDraft);
|
||||
const allDrafts = useInlineCommentDraftStore((state) => state.drafts);
|
||||
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
||||
const newSessionDraftOpen = useSessionStore((state) => state.newSessionDraft?.open);
|
||||
|
||||
const getSessionKey = useCallback(() => {
|
||||
return currentSessionId ?? (newSessionDraftOpen ? 'draft' : null);
|
||||
}, [currentSessionId, newSessionDraftOpen]);
|
||||
return currentSessionId ?? 'draft';
|
||||
}, [currentSessionId]);
|
||||
|
||||
const [selection, setSelection] = useState<SelectedLineRange | null>(null);
|
||||
const [commentText, setCommentText] = useState('');
|
||||
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
|
||||
// to avoid loop with onLineSelected callback
|
||||
const isApplyingSelectionRef = useRef(false);
|
||||
const lastAppliedSelectionRef = useRef<SelectedLineRange | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
selectionRef.current = selection;
|
||||
}, [selection]);
|
||||
|
||||
useEffect(() => {
|
||||
editingDraftIdRef.current = editingDraftId;
|
||||
}, [editingDraftId]);
|
||||
|
||||
const handleSelectionChange = useCallback((range: SelectedLineRange | null) => {
|
||||
// Ignore callbacks while we're programmatically applying selection
|
||||
if (isApplyingSelectionRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const prevSelection = selectionRef.current;
|
||||
|
||||
// Mobile tap-to-extend: if selection exists and new tap is on same side, extend range
|
||||
if (isMobile && selection && range && range.side === selection.side) {
|
||||
const start = Math.min(selection.start, range.start);
|
||||
const end = Math.max(selection.end, range.end);
|
||||
if (isMobile && prevSelection && range && range.side === prevSelection.side) {
|
||||
const start = Math.min(prevSelection.start, range.start);
|
||||
const end = Math.max(prevSelection.end, range.end);
|
||||
setSelection({ ...range, start, end });
|
||||
return;
|
||||
} else {
|
||||
setSelection(range);
|
||||
}
|
||||
|
||||
setSelection(range);
|
||||
|
||||
|
||||
// Clear editing state when selection changes user-driven
|
||||
if (range) {
|
||||
// Don't clear if we're just updating the selection for the same draft?
|
||||
// For now, simple behavior: new selection = new comment flow
|
||||
if (!editingDraftId) {
|
||||
if (!editingDraftIdRef.current) {
|
||||
setCommentText('');
|
||||
}
|
||||
}
|
||||
}, [editingDraftId, isMobile, selection]);
|
||||
}, [isMobile]);
|
||||
|
||||
const handleCancelComment = useCallback(() => {
|
||||
setCommentText('');
|
||||
@@ -258,6 +354,14 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
const targetRange = rangeOverride ?? selection;
|
||||
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();
|
||||
if (!sessionKey) {
|
||||
toast.error('Select a session to save comment');
|
||||
@@ -267,16 +371,16 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
// Pierre selection range: { start, end, side }
|
||||
// Store needs { startLine, endLine, side: 'original'|'modified' }
|
||||
// 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()
|
||||
const selectedText = extractSelectedCode(original, modified, targetRange);
|
||||
const selectedText = extractSelectedCode(original, modified, normalizedRange);
|
||||
|
||||
if (editingDraftId) {
|
||||
updateDraft(sessionKey, editingDraftId, {
|
||||
fileLabel: fileName || 'unknown',
|
||||
startLine: targetRange.start,
|
||||
endLine: targetRange.end,
|
||||
startLine: normalizedRange.start,
|
||||
endLine: normalizedRange.end,
|
||||
side: storeSide,
|
||||
code: selectedText,
|
||||
language: language,
|
||||
@@ -287,8 +391,8 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
sessionKey,
|
||||
source: 'diff',
|
||||
fileLabel: fileName || 'unknown',
|
||||
startLine: targetRange.start,
|
||||
endLine: targetRange.end,
|
||||
startLine: normalizedRange.start,
|
||||
endLine: normalizedRange.end,
|
||||
side: storeSide,
|
||||
code: selectedText,
|
||||
language: language,
|
||||
@@ -325,6 +429,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
const diffRootRef = useRef<HTMLDivElement | null>(null);
|
||||
const diffContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const diffInstanceRef = useRef<PierreFileDiff<unknown> | null>(null);
|
||||
const sharedVirtualizerRef = useRef<SharedVirtualizer | null>(null);
|
||||
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
||||
const workerPool = useWorkerPool();
|
||||
|
||||
@@ -417,6 +522,9 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
hunkSeparators: 'line-info' as const,
|
||||
// Perf: disable intra-line diff (word-level) globally.
|
||||
lineDiffType: 'none' as const,
|
||||
maxLineDiffLength: 1000,
|
||||
maxLineLengthForHighlighting: 1000,
|
||||
expansionLineCount: 20,
|
||||
overflow: wrapLines ? ('wrap' as const) : ('scroll' as const),
|
||||
disableFileHeader: true,
|
||||
enableLineSelection: true,
|
||||
@@ -469,7 +577,12 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
return anns;
|
||||
}, [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(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
@@ -480,9 +593,21 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
// Dispose previous instance
|
||||
diffInstanceRef.current?.cleanUp();
|
||||
diffInstanceRef.current = null;
|
||||
sharedVirtualizerRef.current?.release();
|
||||
sharedVirtualizerRef.current = null;
|
||||
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;
|
||||
lastAppliedSelectionRef.current = null;
|
||||
|
||||
@@ -502,7 +627,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
instance.render({
|
||||
oldFile,
|
||||
newFile,
|
||||
lineAnnotations,
|
||||
lineAnnotations: lineAnnotationsRef.current,
|
||||
containerWrapper: container,
|
||||
});
|
||||
|
||||
@@ -514,9 +639,29 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
if (diffInstanceRef.current === instance) {
|
||||
diffInstanceRef.current = null;
|
||||
}
|
||||
sharedVirtualizer?.release();
|
||||
if (sharedVirtualizer && sharedVirtualizerRef.current === sharedVirtualizer) {
|
||||
sharedVirtualizerRef.current = null;
|
||||
}
|
||||
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(() => {
|
||||
const instance = diffInstanceRef.current;
|
||||
@@ -545,6 +690,64 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
}
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
const container = diffContainerRef.current;
|
||||
@@ -669,12 +872,13 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
|
||||
if (layout === 'fill') {
|
||||
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">
|
||||
<ScrollableOverlay
|
||||
outerClassName="pierre-diff-wrapper size-full"
|
||||
disableHorizontal={false}
|
||||
fillContainer={true}
|
||||
data-diff-virtual-content
|
||||
>
|
||||
<div ref={diffRootRef} className="size-full relative">
|
||||
<div ref={diffContainerRef} className="size-full" />
|
||||
@@ -696,5 +900,3 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@openchamber/ui": "workspace:*",
|
||||
"@opencode-ai/sdk": "^1.1.53",
|
||||
"@opencode-ai/sdk": "^1.1.65",
|
||||
"adm-zip": "^0.5.16",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"react": "^19.1.1",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||
"@ibm/plex": "^6.4.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-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
|
||||
Reference in New Issue
Block a user