import React from 'react'; import { RiCloseLine, RiFullscreenExitLine, RiFullscreenLine } from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { DiffView, FilesView } from '@/components/views'; import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { cn } from '@/lib/utils'; import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore'; import { useUIStore } from '@/stores/useUIStore'; import { ContextPanelContent } from './ContextSidebarTab'; const CONTEXT_PANEL_MIN_WIDTH = 360; const CONTEXT_PANEL_MAX_WIDTH = 1400; const CONTEXT_PANEL_DEFAULT_WIDTH = 600; const normalizeDirectoryKey = (value: string): string => { if (!value) return ''; const raw = value.replace(/\\/g, '/'); const hadUncPrefix = raw.startsWith('//'); let normalized = raw.replace(/\/+$/g, ''); normalized = normalized.replace(/\/+/g, '/'); if (hadUncPrefix && !normalized.startsWith('//')) { normalized = `/${normalized}`; } if (normalized === '') { return raw.startsWith('/') ? '/' : ''; } return normalized; }; const clampWidth = (width: number): number => { if (!Number.isFinite(width)) { return CONTEXT_PANEL_DEFAULT_WIDTH; } return Math.min(CONTEXT_PANEL_MAX_WIDTH, Math.max(CONTEXT_PANEL_MIN_WIDTH, Math.round(width))); }; const getRelativePathLabel = (filePath: string | null, directory: string): string => { if (!filePath) { return ''; } const normalizedFile = filePath.replace(/\\/g, '/'); const normalizedDir = directory.replace(/\\/g, '/').replace(/\/+$/, ''); if (normalizedDir && normalizedFile.startsWith(normalizedDir + '/')) { return normalizedFile.slice(normalizedDir.length + 1); } return normalizedFile; }; export const ContextPanel: React.FC = () => { const effectiveDirectory = useEffectiveDirectory() ?? ''; const directoryKey = React.useMemo(() => normalizeDirectoryKey(effectiveDirectory), [effectiveDirectory]); const panelState = useUIStore((state) => (directoryKey ? state.contextPanelByDirectory[directoryKey] : undefined)); const closeContextPanel = useUIStore((state) => state.closeContextPanel); const toggleContextPanelExpanded = useUIStore((state) => state.toggleContextPanelExpanded); const setContextPanelWidth = useUIStore((state) => state.setContextPanelWidth); const isOpen = Boolean(panelState?.isOpen && panelState?.mode); const isExpanded = Boolean(isOpen && panelState?.expanded); const width = clampWidth(panelState?.width ?? CONTEXT_PANEL_DEFAULT_WIDTH); const [isResizing, setIsResizing] = React.useState(false); const startXRef = React.useRef(0); const startWidthRef = React.useRef(width); const panelRef = React.useRef(null); const wasOpenRef = React.useRef(false); React.useEffect(() => { if (!isOpen || wasOpenRef.current) { wasOpenRef.current = isOpen; return; } const frame = window.requestAnimationFrame(() => { panelRef.current?.focus({ preventScroll: true }); }); wasOpenRef.current = true; return () => window.cancelAnimationFrame(frame); }, [isOpen]); React.useEffect(() => { if (!isResizing || !directoryKey) { return; } const handlePointerMove = (event: PointerEvent) => { const delta = startXRef.current - event.clientX; setContextPanelWidth(directoryKey, startWidthRef.current + delta); }; const handlePointerUp = () => { setIsResizing(false); }; window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp, { once: true }); return () => { window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); }; }, [directoryKey, isResizing, setContextPanelWidth]); const handleResizeStart = React.useCallback((event: React.PointerEvent) => { if (!isOpen || isExpanded || !directoryKey) { return; } setIsResizing(true); startXRef.current = event.clientX; startWidthRef.current = width; event.preventDefault(); }, [directoryKey, isExpanded, isOpen, width]); const handleClose = React.useCallback(() => { if (!directoryKey) { return; } closeContextPanel(directoryKey); }, [closeContextPanel, directoryKey]); const handleToggleExpanded = React.useCallback(() => { if (!directoryKey) { return; } toggleContextPanelExpanded(directoryKey); }, [directoryKey, toggleContextPanelExpanded]); const handlePanelKeyDownCapture = React.useCallback((event: React.KeyboardEvent) => { if (event.key !== 'Escape') { return; } event.preventDefault(); event.stopPropagation(); handleClose(); }, [handleClose]); const activeFilePath = useFilesViewTabsStore((state) => (directoryKey ? (state.byRoot[directoryKey]?.selectedPath ?? null) : null)); const panelTitle = panelState?.mode === 'diff' ? 'Diff' : panelState?.mode === 'file' ? 'File' : panelState?.mode === 'context' ? 'Context' : 'Panel'; const effectivePath = panelState?.mode === 'file' ? (activeFilePath ?? panelState?.targetPath ?? null) : panelState?.mode === 'context' ? null : (panelState?.targetPath ?? null); const pathLabel = getRelativePathLabel(effectivePath, effectiveDirectory); const content = panelState?.mode === 'diff' ? : panelState?.mode === 'file' ? : panelState?.mode === 'context' ? : null; const header = (
{panelTitle} {pathLabel ? {pathLabel} : null}
); if (!isOpen) { return null; } return ( ); };