diff --git a/packages/ui/src/components/layout/ContextPanel.tsx b/packages/ui/src/components/layout/ContextPanel.tsx index bd91b5ab..7ab0b581 100644 --- a/packages/ui/src/components/layout/ContextPanel.tsx +++ b/packages/ui/src/components/layout/ContextPanel.tsx @@ -2283,11 +2283,11 @@ export const ContextPanel: React.FC = () => { key={activeTab.id} hideStackedFileSidebar stackedDefaultCollapsedAll - hideFileSelector pinSelectedFileHeaderToTopOnNavigate showOpenInEditorAction diffScope={activeTab.stagedDiff ? 'staged' : 'working'} targetFilePath={activeTab.targetPath} + flushContent /> ) : activeTab?.mode === 'context' diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index 7bf41847..f55f1cf9 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -73,19 +73,6 @@ const DIFF_LAYOUT_OPTIONS: Option<'dynamic' | 'inline' | 'side-by-side'>[] = [ }, ]; -const DIFF_VIEW_MODE_OPTIONS: Option<'single' | 'stacked'>[] = [ - { - id: 'single', - labelKey: 'settings.openchamber.visual.option.diffViewMode.single.label', - descriptionKey: 'settings.openchamber.visual.option.diffViewMode.single.description', - }, - { - id: 'stacked', - labelKey: 'settings.openchamber.visual.option.diffViewMode.stacked.label', - descriptionKey: 'settings.openchamber.visual.option.diffViewMode.stacked.description', - }, -]; - const MERMAID_RENDERING_OPTIONS: Option<'svg' | 'ascii'>[] = [ { id: 'svg', @@ -297,8 +284,6 @@ export const OpenChamberVisualSettings: React.FC const setMobileKeyboardMode = useUIStore(state => state.setMobileKeyboardMode); const diffLayoutPreference = useUIStore(state => state.diffLayoutPreference); const setDiffLayoutPreference = useUIStore(state => state.setDiffLayoutPreference); - const diffViewMode = useUIStore(state => state.diffViewMode); - const setDiffViewMode = useUIStore(state => state.setDiffViewMode); const showTerminalQuickKeysOnDesktop = useUIStore(state => state.showTerminalQuickKeysOnDesktop); const setShowTerminalQuickKeysOnDesktop = useUIStore(state => state.setShowTerminalQuickKeysOnDesktop); const fileEditorKeymap = useUIStore(state => state.fileEditorKeymap); @@ -1696,41 +1681,6 @@ export const OpenChamberVisualSettings: React.FC )} - {shouldShow('diffLayout') && !isVSCode && ( -
-

{t('settings.openchamber.visual.section.diffViewMode')}

-
- {DIFF_VIEW_MODE_OPTIONS.map((option) => { - const selected = diffViewMode === option.id; - return ( -
setDiffViewMode(option.id)} - onKeyDown={(event) => { - if (event.key === ' ' || event.key === 'Enter') { - event.preventDefault(); - setDiffViewMode(option.id); - } - }} - className="flex w-full items-center gap-2 py-0 text-left" - > - setDiffViewMode(option.id)} - ariaLabel={t('settings.openchamber.visual.field.diffViewModeAria', { option: tUnsafe(option.labelKey) })} - /> - - {tUnsafe(option.labelKey)} - -
- ); - })} -
-
- )} )} diff --git a/packages/ui/src/components/views/DiffView.tsx b/packages/ui/src/components/views/DiffView.tsx index 927cd5ff..b37fa329 100644 --- a/packages/ui/src/components/views/DiffView.tsx +++ b/packages/ui/src/components/views/DiffView.tsx @@ -8,10 +8,8 @@ import type { GitStatus } from '@/lib/api/types'; import { DropdownMenu, DropdownMenuContent, - DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, - DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Button } from '@/components/ui/button'; @@ -64,8 +62,6 @@ const BinaryDiffPlaceholder = React.memo(() => { ); }); -type DiffTabViewMode = 'single' | 'stacked'; - type ChangeDescriptor = { code: string; color: string; @@ -83,23 +79,6 @@ const CHANGE_DESCRIPTORS: Record = { const DEFAULT_CHANGE_DESCRIPTOR = CHANGE_DESCRIPTORS.M; -const DIFF_VIEW_MODE_OPTIONS: Array<{ - value: DiffTabViewMode; - labelKey: I18nKey; - descriptionKey: I18nKey; -}> = [ - { - value: 'single', - labelKey: 'diffView.mode.single.label', - descriptionKey: 'diffView.mode.single.description', - }, - { - value: 'stacked', - labelKey: 'diffView.mode.stacked.label', - descriptionKey: 'diffView.mode.stacked.description', - }, -]; - const getChangeSymbol = (file: GitStatus['files'][number]): string => { const indexCode = file.index?.trim(); const workingCode = file.working_dir?.trim(); @@ -189,15 +168,50 @@ const formatDiffTotals = (insertions?: number, deletions?: number) => { ); }; +const DiffFilePathLabel = React.memo<{ + path: string; + className?: string; +}>(({ path, className }) => { + const lastSlash = path.lastIndexOf('/'); + if (lastSlash === -1) { + return ( + + {path} + + ); + } + + const dir = path.slice(0, lastSlash); + const name = path.slice(lastSlash + 1); + + return ( + + + {dir} + + + / + {name} + + + ); +}); + interface FileSelectorProps { changedFiles: FileEntry[]; selectedFile: string | null; selectedFileEntry: FileEntry | null; onSelectFile: (path: string) => void; - isMobile: boolean; - showModeSelector?: boolean; - mode?: DiffTabViewMode; - onModeChange?: (mode: DiffTabViewMode) => void; } const FileSelector = React.memo(({ @@ -205,30 +219,19 @@ const FileSelector = React.memo(({ selectedFile, selectedFileEntry, onSelectFile, - isMobile, - showModeSelector = false, - mode, - onModeChange, }) => { const { t } = useI18n(); - const getLabel = React.useCallback((path: string) => { - if (!isMobile) return path; - const lastSlash = path.lastIndexOf('/'); - return lastSlash >= 0 ? path.slice(lastSlash + 1) : path; - }, [isMobile]); if (changedFiles.length === 0) return null; return ( - - - {showModeSelector && mode && onModeChange ? ( - <> - - {t('diffView.selector.viewMode')} - - onModeChange(value as DiffTabViewMode)} - > - {DIFF_VIEW_MODE_OPTIONS.map((option) => ( - - - {t(option.labelKey)} - - - ))} - - - - ) : null} + {changedFiles.map((file) => ( - +
- - {getLabel(file.path)} - - - {formatDiffTotals(file.insertions, file.deletions)} - + + {formatDiffTotals(file.insertions, file.deletions)}
))} @@ -282,44 +257,6 @@ const FileSelector = React.memo(({ ); }); -interface DiffViewModeSelectorProps { - mode: DiffTabViewMode; - onModeChange: (mode: DiffTabViewMode) => void; -} - -const DiffViewModeSelector = React.memo(({ mode, onModeChange }) => { - const { t } = useI18n(); - const currentOption = - DIFF_VIEW_MODE_OPTIONS.find((option) => option.value === mode) ?? DIFF_VIEW_MODE_OPTIONS[0]; - - return ( - - - - - - onModeChange(value as DiffTabViewMode)} - > - {DIFF_VIEW_MODE_OPTIONS.map((option) => ( - - - {t(option.labelKey)} - - - ))} - - - - ); -}); - interface FileListProps { changedFiles: FileEntry[]; selectedFile: string | null; @@ -380,68 +317,6 @@ const FileList = React.memo(({ }); // Image diff viewer for binary image files -interface ImageDiffViewerProps { - filePath: string; - diff: DiffData; - isVisible: boolean; - renderSideBySide: boolean; -} - -const ImageDiffViewer = React.memo(({ - filePath, - diff, - isVisible, - renderSideBySide, -}) => { - const { t } = useI18n(); - const hasOriginal = diff.original.length > 0; - const hasModified = diff.modified.length > 0; - - if (!isVisible) { - return
; - } - - // Render side-by-side or stacked based on preference - const containerClass = renderSideBySide - ? 'flex flex-row gap-6 items-start justify-center h-full' - : 'flex flex-col gap-4 items-center'; - - const imageContainerClass = renderSideBySide - ? 'flex flex-col items-center gap-2 flex-1 min-w-0 h-full' - : 'flex flex-col items-center gap-2'; - - return ( -
-
- {hasOriginal && ( -
- {t('diffView.image.original')} - {t('diffView.image.originalAlt', -
- )} - {hasModified && ( -
- - {hasOriginal ? t('diffView.image.modified') : t('diffView.image.new')} - - {t('diffView.image.modifiedAlt', -
- )} -
-
- ); -}); - interface InlineImageDiffViewerProps { filePath: string; diff: DiffData; @@ -544,70 +419,6 @@ const InlineDiffViewer = React.memo(({ ); }); -// Single diff viewer instance -interface SingleDiffViewerProps { - filePath: string; - diff: DiffData; - isVisible: boolean; - renderSideBySide: boolean; - wrapLines: boolean; -} - -const SingleDiffViewer = React.memo(({ - filePath, - diff, - isVisible, - renderSideBySide, - wrapLines, -}) => { - const language = React.useMemo( - () => getLanguageFromExtension(filePath) || 'text', - [filePath] - ); - - if (diff.isBinary) { - return ; - } - - // Don't render if not visible (memory optimization) - if (!isVisible) { - return null; - } - - // Check if this is an image file - if (isImageFile(filePath)) { - return ( - - ); - } - - return ( - - - - ); -}); - interface MultiFileDiffEntryProps { directory: string; file: FileEntry; @@ -794,20 +605,19 @@ const MultiFileDiffEntry = React.memo(({ }, [handleOpenChange, handleSelect, isExpanded]); return ( -
-
+
+
{isExpanded && ( -
+
{diffLoadError ? (
@@ -961,6 +771,8 @@ interface DiffViewProps { showOpenInEditorAction?: boolean; diffScope?: DiffScope; targetFilePath?: string | null; + /** Render diff content flush with the container edges (no outer padding). */ + flushContent?: boolean; } export const DiffView: React.FC = ({ @@ -971,6 +783,7 @@ export const DiffView: React.FC = ({ showOpenInEditorAction = false, diffScope = 'all', targetFilePath = null, + flushContent = false, }) => { const { t } = useI18n(); const { git, files } = useRuntimeAPIs(); @@ -995,9 +808,6 @@ export const DiffView: React.FC = ({ const [stackedExpandTarget, setStackedExpandTarget] = React.useState(null); const [stackedExpandRequestNonce, setStackedExpandRequestNonce] = React.useState(0); const [pinnedStackedTarget, setPinnedStackedTarget] = React.useState(null); - const [diffRetryNonce, setDiffRetryNonce] = React.useState(0); - const [diffLoadError, setDiffLoadError] = React.useState(null); - const lastDiffRequestRef = React.useRef(null); const pendingDiffFile = useUIStore((state) => state.pendingDiffFile); const pendingDiffStaged = useUIStore((state) => state.pendingDiffStaged); @@ -1007,14 +817,11 @@ export const DiffView: React.FC = ({ const setDiffFileLayout = useUIStore((state) => state.setDiffFileLayout); const diffWrapLinesStore = useUIStore((state) => state.diffWrapLines); const setDiffWrapLines = useUIStore((state) => state.setDiffWrapLines); - const diffViewMode = useUIStore((state) => state.diffViewMode); - const setDiffViewMode = useUIStore((state) => state.setDiffViewMode); const openContextFileAtLine = useUIStore((state) => state.openContextFileAtLine); const diffWrapLines = diffWrapLinesStore; const forcedStaged = diffScope === 'staged' ? true : diffScope === 'working' ? false : null; const activeDiffStaged = forcedStaged ?? selectedFileStaged; - const isStackedView = diffViewMode === 'stacked'; const isMobileLayout = isMobile || screenWidth <= 768; const showFileSidebar = !hideStackedFileSidebar && !isMobileLayout && screenWidth >= 1024; const diffScrollRef = React.useRef(null); @@ -1024,7 +831,7 @@ export const DiffView: React.FC = ({ const shouldPinAfterAlignRef = React.useRef(false); React.useEffect(() => { - if (!pinSelectedFileHeaderToTopOnNavigate || !isStackedView || !pinnedStackedTarget) { + if (!pinSelectedFileHeaderToTopOnNavigate || !pinnedStackedTarget) { return; } @@ -1116,7 +923,7 @@ export const DiffView: React.FC = ({ window.removeEventListener('keydown', cancelOnUserInput, true); scrollRoot.removeEventListener('scroll', cancelOnScroll); }; - }, [isStackedView, pinSelectedFileHeaderToTopOnNavigate, pinnedStackedTarget]); + }, [pinSelectedFileHeaderToTopOnNavigate, pinnedStackedTarget]); const changedFiles: FileEntry[] = React.useMemo(() => { if (!status?.files) return []; @@ -1200,14 +1007,12 @@ export const DiffView: React.FC = ({ setSelectedFileStaged(pendingDiffStaged); setSelectedStagedDiffData(null); setPendingDiffFile(null); - if (isStackedView) { - shouldPinAfterAlignRef.current = true; - pendingScrollTargetRef.current = pendingDiffFile; - setStackedExpandTarget(pendingDiffFile); - setStackedExpandRequestNonce((nonce) => nonce + 1); - } + shouldPinAfterAlignRef.current = true; + pendingScrollTargetRef.current = pendingDiffFile; + setStackedExpandTarget(pendingDiffFile); + setStackedExpandRequestNonce((nonce) => nonce + 1); } - }, [diffScope, isStackedView, pendingDiffFile, pendingDiffStaged, setPendingDiffFile]); + }, [diffScope, pendingDiffFile, pendingDiffStaged, setPendingDiffFile]); React.useEffect(() => { if (diffScope === 'all') { @@ -1223,13 +1028,11 @@ export const DiffView: React.FC = ({ setSelectedFileStaged(diffScope === 'staged'); setSelectedStagedDiffData(null); - if (isStackedView) { - shouldPinAfterAlignRef.current = true; - pendingScrollTargetRef.current = normalizedTarget; - setStackedExpandTarget(normalizedTarget); - setStackedExpandRequestNonce((nonce) => nonce + 1); - } - }, [diffScope, isStackedView, targetFilePath]); + shouldPinAfterAlignRef.current = true; + pendingScrollTargetRef.current = normalizedTarget; + setStackedExpandTarget(normalizedTarget); + setStackedExpandRequestNonce((nonce) => nonce + 1); + }, [diffScope, targetFilePath]); React.useEffect(() => { if (!activeDiffStaged) { @@ -1237,8 +1040,6 @@ export const DiffView: React.FC = ({ } setSelectedStagedDiffData(null); - setDiffLoadError(null); - lastDiffRequestRef.current = null; }, [activeDiffStaged, indexRevision]); // Auto-select first file (skip if we have a pending file to consume) @@ -1299,16 +1100,6 @@ export const DiffView: React.FC = ({ }, []); React.useEffect(() => { - if (!isStackedView) { - pendingScrollTargetRef.current = null; - shouldPinAfterAlignRef.current = false; - if (pendingScrollFrameRef.current !== null) { - window.cancelAnimationFrame(pendingScrollFrameRef.current); - pendingScrollFrameRef.current = null; - } - return; - } - const target = pendingScrollTargetRef.current; if (!target) return; @@ -1431,7 +1222,7 @@ export const DiffView: React.FC = ({ pendingScrollFrameRef.current = null; } }; - }, [isStackedView, pinSelectedFileHeaderToTopOnNavigate, scrollToFile, selectedFile, stackedExpandRequestNonce]); + }, [pinSelectedFileHeaderToTopOnNavigate, scrollToFile, selectedFile, stackedExpandRequestNonce]); const handleSelectFile = React.useCallback((value: string) => { setSelectedFile(value); @@ -1450,43 +1241,23 @@ export const DiffView: React.FC = ({ setSelectedFileStaged(false); setSelectedStagedDiffData(null); - if (!isStackedView) { - shouldPinAfterAlignRef.current = false; - return; - } - shouldPinAfterAlignRef.current = true; pendingScrollTargetRef.current = value; + setStackedExpandTarget(value); + setStackedExpandRequestNonce((nonce) => nonce + 1); scrollToFile(value); - }, [isStackedView, scrollToFile]); - - const handleDiffViewModeChange = React.useCallback((mode: DiffTabViewMode) => { - setDiffViewMode(mode); - if (mode === 'stacked' && selectedFile) { - const result = scrollToFile(selectedFile); - if (!result.aligned) { - pendingScrollTargetRef.current = selectedFile; - } - } - }, [scrollToFile, selectedFile, setDiffViewMode]); + }, [scrollToFile]); const handleHeaderLayoutChange = React.useCallback((mode: DiffViewMode) => { const nextLayout: 'inline' | 'side-by-side' = mode === 'side-by-side' ? 'side-by-side' : 'inline'; - if (isStackedView) { - changedFiles.forEach((file) => { - setDiffFileLayout(file.path, nextLayout); - }); - return; - } + changedFiles.forEach((file) => { + setDiffFileLayout(file.path, nextLayout); + }); + }, [changedFiles, setDiffFileLayout]); - if (!selectedFileEntry) return; - setDiffFileLayout(selectedFileEntry.path, nextLayout); - }, [changedFiles, isStackedView, selectedFileEntry, setDiffFileLayout]); - - const renderSideBySide = (currentLayoutForSelectedFile ?? 'side-by-side') === 'side-by-side'; - const showFileSelector = !hideFileSelector && (!isStackedView || !showFileSidebar); + const showFileSelector = !hideFileSelector && !showFileSidebar; const selectedCachedDiff = useGitStore(React.useCallback((state) => { if (!effectiveDirectory || !selectedFile || activeDiffStaged) return null; @@ -1572,85 +1343,6 @@ export const DiffView: React.FC = ({ const isOpeningSelectedInEditor = Boolean(selectedFile && openingEditorFilePath === selectedFile); - const hasCurrentDiff = activeDiffStaged ? !!selectedStagedDiffData : !!selectedCachedDiff; - const isCurrentFileLoading = !isStackedView && !!selectedFile && !hasCurrentDiff; - - React.useEffect(() => { - if (isStackedView) { - return; - } - - setDiffLoadError(null); - - if (!effectiveDirectory || !selectedFile) { - lastDiffRequestRef.current = null; - return; - } - - if (activeDiffStaged ? selectedStagedDiffData : selectedCachedDiff) { - lastDiffRequestRef.current = null; - return; - } - - const requestKey = `${effectiveDirectory}::${selectedFile}::${activeDiffStaged ? `staged:${indexRevision}` : 'unstaged'}::${diffRetryNonce}`; - if (lastDiffRequestRef.current === requestKey) { - return; - } - lastDiffRequestRef.current = requestKey; - - let cancelled = false; - const fetchPromise = git.getGitFileDiff(effectiveDirectory, { path: selectedFile, staged: activeDiffStaged }); - const timeoutMs = DIFF_REQUEST_TIMEOUT_MS; - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs); - }); - - void Promise.race([fetchPromise, timeoutPromise]) - .then((response) => { - if (cancelled) return; - - const nextDiff = { - original: response.original ?? '', - modified: response.modified ?? '', - isBinary: response.isBinary, - }; - if (activeDiffStaged) { - setSelectedStagedDiffData(nextDiff); - } else { - setDiff(effectiveDirectory, selectedFile, nextDiff); - } - }) - .catch((error) => { - if (cancelled) return; - const message = error instanceof Error ? error.message : String(error); - setDiffLoadError(message); - }); - - return () => { - cancelled = true; - if (lastDiffRequestRef.current === requestKey) { - // Allow a retry if this request was cancelled due to directory/path churn. - lastDiffRequestRef.current = null; - } - }; - }, [activeDiffStaged, effectiveDirectory, indexRevision, isStackedView, selectedFile, selectedCachedDiff, selectedStagedDiffData, git, setDiff, diffRetryNonce]); - - // Render only the selected diff viewer to prevent memory bloat with many files - const renderSelectedDiffViewer = () => { - if (!effectiveDirectory || !selectedFile || !selectedDiffData) return null; - - return ( - - ); - }; - const renderStackedDiffView = () => { if (!effectiveDirectory) return null; @@ -1663,7 +1355,7 @@ export const DiffView: React.FC = ({ }; return ( -
+
{showFileSidebar && (
@@ -1680,14 +1372,13 @@ export const DiffView: React.FC = ({ -
+
{changedFiles.map((file, index) => ( = ({ ); } - if (isStackedView) { - return renderStackedDiffView(); - } - - return ( -
- {renderSelectedDiffViewer()} - {isCurrentFileLoading && !hasCurrentDiff && ( -
- {diffLoadError ? ( -
-
- {t('diffView.state.failedToLoadDiff')} -
-
- {diffLoadError} -
- -
- ) : ( - <> - - {t('diffView.state.loadingDiff')} - - )} -
- )} -
- ); + return renderStackedDiffView(); }; return ( @@ -1807,19 +1461,12 @@ export const DiffView: React.FC = ({
)} - {!isMobileLayout && ( - - )} {showFileSelector && ( )}
@@ -1837,7 +1484,7 @@ export const DiffView: React.FC = ({ )} - {showOpenInEditorAction && selectedFileEntry && !isStackedView && ( + {showOpenInEditorAction && selectedFileEntry && (