feat(UI): Introduce new UI components for file attachments and related views (#191)

Add file management API and UI components
Implement directory listing, search and CRUD operations in desktop backend
Expose new Files API on frontend to list, search, and modify files
This commit is contained in:
Bohdan Triapitsyn
2026-01-22 12:21:19 +02:00
committed by GitHub
parent 1f23b63c0b
commit d97181bcd2
22 changed files with 1611 additions and 1223 deletions
@@ -2,7 +2,7 @@ import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react'
import { createPortal } from 'react-dom';
import { FileDiff } from '@pierre/diffs/react';
import { parseDiffFromFile, type FileContents, type FileDiffMetadata, type SelectedLineRange } from '@pierre/diffs';
import { RiArrowDownSLine, RiEyeLine, RiSendPlane2Line } from '@remixicon/react';
import { RiSendPlane2Line } from '@remixicon/react';
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
@@ -141,7 +141,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
const [selection, setSelection] = useState<SelectedLineRange | null>(null);
const [commentText, setCommentText] = useState('');
const commentContainerRef = useRef<HTMLDivElement>(null);
// Calculate initial center synchronously to avoid flicker
const getMainContentCenter = useCallback(() => {
@@ -212,8 +212,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
const target = e.target as HTMLElement;
// Check if click is inside the comment UI portal
const commentUI = document.querySelector('[data-comment-ui]');
if (commentUI?.contains(target)) return;
if (commentContainerRef.current?.contains(target)) return;
// Check if click is inside toast (sonner)
if (target.closest('[data-sonner-toast]') || target.closest('[data-sonner-toaster]')) return;
@@ -302,21 +301,8 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
fileDiff: FileDiffMetadata;
} | null>(null);
// Threshold for lazy loading (total lines > 1500 or content size > 150KB)
const isLargeDiff = useMemo(() => {
const totalLines = (original || '').split('\n').length + (modified || '').split('\n').length;
const totalSize = (original?.length || 0) + (modified?.length || 0);
return totalLines > 1500 || totalSize > 150 * 1024;
}, [original, modified]);
// State for large diff loading
const [shouldLoad, setShouldLoad] = useState(false);
// Parse diff when loaded (for large diffs) or always (for small diffs)
// Pre-parse the diff with cacheKey for worker pool caching
const fileDiff = useMemo(() => {
// For large diffs, only parse if manually triggered
if (isLargeDiff && !shouldLoad) return null;
const cacheKey = getCacheKey(fileName, original, modified);
// Return cached diff if inputs haven't changed
@@ -344,7 +330,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
diffCacheRef.current = { key: cacheKey, fileDiff: diff };
return diff;
}, [fileName, original, modified, language, isLargeDiff, shouldLoad]);
}, [fileName, original, modified, language]);
const options = useMemo(() => ({
theme: {
@@ -363,45 +349,11 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
onLineSelected: handleSelectionChange,
unsafeCSS: WEBKIT_SCROLL_FIX_CSS,
}), [isDark, renderSideBySide, wrapLines, handleSelectionChange]);
// Show placeholder for large diffs
if (isLargeDiff && !fileDiff) {
const originalLines = (original || '').split('\n').length;
const modifiedLines = (modified || '').split('\n').length;
return (
<div className="flex flex-col items-center justify-center h-full min-h-[200px] p-6">
<div className="flex flex-col items-center gap-3 text-center">
<div className="p-3 rounded-full bg-muted/50">
<RiEyeLine className="size-6 text-muted-foreground" />
</div>
<div>
<div className="typography-ui-header font-medium text-foreground">Large Diff</div>
<div className="typography-meta text-muted-foreground mt-1">
{originalLines + modifiedLines} lines
</div>
</div>
<button
type="button"
onClick={() => setShouldLoad(true)}
className="flex items-center gap-2 px-4 py-2 mt-2 rounded-lg bg-primary/10 text-primary hover:bg-primary/20 transition-colors typography-meta font-medium"
>
<RiArrowDownSLine className="size-4" />
Load Diff
</button>
</div>
</div>
);
}
if (typeof window === 'undefined') {
return null;
}
// fileDiff should not be null here (handled by large diff placeholder above)
if (!fileDiff) {
return null;
}
// Extracted Comment Interface Content for reuse in Portal or In-Flow
const renderCommentContent = () => {
if (!selection) return null;
@@ -521,6 +473,8 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
: '16px'
}}
data-keyboard-avoid="true"
data-comment-ui="true"
ref={commentContainerRef}
>
{commentContent}
</div>
@@ -566,6 +520,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
}}
data-keyboard-avoid="true"
data-comment-ui="true"
ref={commentContainerRef}
>
{commentContent}
</div>