import { useEffect, useRef, useState } from 'react' import { Folder, File as FileIcon, ChevronRight, Upload, FolderPlus, Trash2, Download, Pencil, Save, X, RefreshCw, } from 'lucide-react' import { api, type FileEntry, type Integration } from '../lib/api' const TEXT_PRIMARY = '#E8E6E0' const TEXT_SECONDARY = '#7A7D85' const GOLD = '#C8A434' const cardBase: React.CSSProperties = { backgroundColor: 'rgba(10, 10, 12, 0.92)', border: '1px solid rgba(200, 164, 52, 0.08)', borderRadius: '12px', boxShadow: '0 0 20px rgba(200, 164, 52, 0.03)', } function inputStyle(): React.CSSProperties { return { backgroundColor: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '6px', padding: '6px 10px', color: TEXT_PRIMARY, fontSize: '13px', } } function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B` const units = ['KB', 'MB', 'GB', 'TB'] let v = bytes let i = -1 do { v /= 1024 i++ } while (v >= 1024 && i < units.length - 1) return `${v.toFixed(1)} ${units[i]}` } function joinPath(dir: string, name: string): string { if (dir === '.' || dir === '') return name return `${dir.replace(/\/$/, '')}/${name}` } export default function Files() { const [hosts, setHosts] = useState([]) const [integrationId, setIntegrationId] = useState('') const [path, setPath] = useState('.') const [entries, setEntries] = useState([]) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [editingPath, setEditingPath] = useState(null) const [editingContent, setEditingContent] = useState('') const [editingEncoding, setEditingEncoding] = useState<'utf8' | 'base64'>('utf8') const [savingEdit, setSavingEdit] = useState(false) const fileInputRef = useRef(null) useEffect(() => { api.listIntegrations().then(({ integrations }) => { const sshHosts = integrations.filter((i) => i.type === 'ssh') setHosts(sshHosts) if (sshHosts.length > 0) setIntegrationId(sshHosts[0].id) }) }, []) function refresh() { if (!integrationId) return setLoading(true) setError(null) api .listFiles(integrationId, path) .then(({ entries }) => setEntries(entries)) .catch((err) => setError(err instanceof Error ? err.message : 'Failed to list directory')) .finally(() => setLoading(false)) } useEffect(() => { refresh() // eslint-disable-next-line react-hooks/exhaustive-deps }, [integrationId, path]) function openDirectory(name: string) { setPath(joinPath(path, name)) } function goUp() { if (path === '.' || path === '') return const parts = path.split('/').filter(Boolean) parts.pop() setPath(parts.length === 0 ? '.' : parts.join('/')) } async function openFile(name: string) { if (!integrationId) return const full = joinPath(path, name) setError(null) try { const result = await api.readFile(integrationId, full) setEditingPath(full) setEditingContent(result.content) setEditingEncoding(result.encoding) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to open file') } } async function saveEdit() { if (!integrationId || !editingPath) return setSavingEdit(true) try { await api.writeFile(integrationId, editingPath, editingContent, editingEncoding) setEditingPath(null) refresh() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save file') } finally { setSavingEdit(false) } } async function handleMkdir() { if (!integrationId) return const name = prompt('New folder name') if (!name) return try { await api.mkdir(integrationId, joinPath(path, name)) refresh() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create directory') } } async function handleRename(entry: FileEntry) { if (!integrationId) return const next = prompt('Rename to', entry.name) if (!next || next === entry.name) return try { await api.renameFile(integrationId, joinPath(path, entry.name), joinPath(path, next)) refresh() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to rename') } } async function handleDelete(entry: FileEntry) { if (!integrationId) return if (!confirm(`Delete ${entry.name}?`)) return try { await api.deleteFile(integrationId, joinPath(path, entry.name), entry.isDirectory) refresh() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete') } } function handleDownload(entry: FileEntry) { if (!integrationId) return window.open(api.downloadFileUrl(integrationId, joinPath(path, entry.name)), '_blank') } async function handleUpload(file: File) { if (!integrationId) return setError(null) try { await api.uploadFile(integrationId, path, file) refresh() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to upload file') } } const breadcrumbs = path === '.' || path === '' ? [] : path.split('/').filter(Boolean) return (

Files

Browse, edit, and transfer files on your remote SSH hosts.

{error && (
{error}
)}
{breadcrumbs.map((part, i) => ( ))}
{ const file = e.target.files?.[0] if (file) handleUpload(file) e.target.value = '' }} />
{path !== '.' && path !== '' && ( )} {entries.map((entry) => ( ))} {entries.length === 0 && !loading && ( )}
../
(entry.isDirectory ? openDirectory(entry.name) : openFile(entry.name))} > {entry.isDirectory ? : } {entry.name} {entry.isDirectory ? '' : formatSize(entry.size)} {(entry.mode & 0o777).toString(8)}
{!entry.isDirectory && ( )}
Empty directory
{editingPath && (
{editingPath}
{editingEncoding === 'base64' ? (
Binary file - editing not supported. Use download instead.
) : (