import { useCallback, useEffect, useRef, useState } from 'react'; import { HardDrive, RefreshCw } from 'lucide-react'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button'; import { toast } from '@/components/ui/toast-store'; import { FileTree } from '@/components/files/FileTree'; import type { FileEntry } from '@/lib/stackFilesApi'; import { listVolumeDirectory, readVolumeFile } from '@/lib/volumeApi'; import type { VolumeFileResult } from '@/lib/volumeApi'; import { formatBytes } from '@/lib/utils'; interface VolumeBrowserSheetProps { volumeName: string | null; onClose: () => void; } export function VolumeBrowserSheet({ volumeName, onClose }: VolumeBrowserSheetProps) { const [refreshKey, setRefreshKey] = useState(0); const [selectedPath, setSelectedPath] = useState(''); const [fileLoading, setFileLoading] = useState(false); const [fileResult, setFileResult] = useState(null); // Generation counter so a slow read for a stale (volume, path) selection // cannot stomp the visible result after the user has moved on. const readGenerationRef = useRef(0); useEffect(() => { // Cancel any in-flight read when the volume context changes. readGenerationRef.current += 1; }, [volumeName]); const handleSelectFile = useCallback(async (relPath: string) => { if (!volumeName) return; const generation = ++readGenerationRef.current; const targetVolume = volumeName; setSelectedPath(relPath); setFileLoading(true); setFileResult(null); try { const result = await readVolumeFile(targetVolume, relPath); if (readGenerationRef.current !== generation) return; setFileResult(result); } catch (err: unknown) { if (readGenerationRef.current !== generation) return; const msg = err instanceof Error ? err.message : 'Failed to read file.'; toast.error(msg); } finally { if (readGenerationRef.current === generation) setFileLoading(false); } }, [volumeName]); const handleClose = (open: boolean) => { if (!open) { setSelectedPath(''); setFileResult(null); setFileLoading(false); onClose(); } }; const loadDir = useCallback( (relPath: string) => { if (!volumeName) return Promise.resolve([]); return listVolumeDirectory(volumeName, relPath); }, [volumeName] ); return ( {volumeName} {volumeName && (
{!selectedPath && (
Select a file to preview.
)} {selectedPath && fileLoading && (
)} {selectedPath && !fileLoading && fileResult && ( )}
)}

File reads are recorded in the audit log.

); } function FileResultPanel({ path, result }: { path: string; result: VolumeFileResult }) { const decoded = result.binary ? base64ToHex(result.content) : result.content; return (
{path} {formatBytes(result.size)}{result.binary ? ' · binary' : ''}{result.truncated ? ' · truncated' : ''}
{result.truncated && (
Showing first {formatBytes(5 * 1024 * 1024)}. Larger files cannot be downloaded from this view.
)}
        {decoded}
      
); } function base64ToHex(b64: string): string { try { const binary = atob(b64); const out: string[] = []; for (let i = 0; i < binary.length; i += 16) { const offset = i.toString(16).padStart(8, '0'); const chunk = binary.slice(i, i + 16); const hex = Array.from(chunk).map((c) => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' '); const ascii = Array.from(chunk).map((c) => { const code = c.charCodeAt(0); return code >= 32 && code <= 126 ? c : '.'; }).join(''); out.push(`${offset} ${hex.padEnd(48, ' ')} ${ascii}`); } return out.join('\n'); } catch { return '(unable to decode binary content)'; } }