diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index 0f7e5cd833..9574f014be 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -121,10 +121,7 @@ const normalizePath = (path: string) => path.replace(/\\/g, '/').replace(/\/{2,}/g, '/'); const cleanEntryName = (name: string) => - normalizePath(name) - .split('/') - .filter(Boolean) - .at(-1) ?? name.replace(/\/+$/, ''); + normalizePath(name).split('/').filter(Boolean).at(-1) ?? name.replace(/\/+$/, ''); const normalizeEntries = (items: FileEntry[]) => items.map((entry) => ({ ...entry, name: cleanEntryName(entry.name) })); @@ -498,11 +495,7 @@ } const pathForHome = hintPath && hintPath !== '/' ? hintPath : cwdPath; - if ( - homePath && - pathForHome && - (pathForHome === homePath || pathForHome.startsWith(homePath)) - ) { + if (homePath && pathForHome && (pathForHome === homePath || pathForHome.startsWith(homePath))) { return { path: homePath, label: 'Home' }; } @@ -535,11 +528,11 @@ const isDrive = /^[A-Za-z]:$/.test(parts[0] ?? ''); const root = isDrive ? { label: parts[0], path: `${parts[0]}/` } : { label: '/', path: '/' }; return (isDrive ? parts.slice(1) : parts).reduce( - (acc, part) => { - const prev = acc[acc.length - 1]; - acc.push({ label: part, path: asDirectoryPath(joinPath(prev.path, part)) }); - return acc; - }, + (acc, part) => { + const prev = acc[acc.length - 1]; + acc.push({ label: part, path: asDirectoryPath(joinPath(prev.path, part)) }); + return acc; + }, [root] ); }; @@ -571,7 +564,10 @@ }; // ── Directory operations ───────────────────────────────────────────── - const loadDir = async (path: string, options: { preserveTree?: boolean; restoreTree?: boolean } = {}) => { + const loadDir = async ( + path: string, + options: { preserveTree?: boolean; restoreTree?: boolean } = {} + ) => { const terminal = selectedTerminal; if (!terminal) return; const directory = clampToFileRoot(path); @@ -663,7 +659,8 @@ }; const openEntry = async (entry: FileEntry) => { - const fullPath = 'fullPath' in entry ? (entry as BrowserRow).fullPath : entryPath(currentPath, entry); + const fullPath = + 'fullPath' in entry ? (entry as BrowserRow).fullPath : entryPath(currentPath, entry); const parentPath = 'parentPath' in entry ? (entry as BrowserRow).parentPath : currentPath; if (entry.type === 'directory') { await loadDir(fullPath); @@ -819,8 +816,8 @@ if (rawMove) { try { const data = JSON.parse(rawMove); - const paths = data.paths || (data.path ? [data.path] : []); - for (const path of paths) await handleMove(path, currentPath); + const paths = (data.paths || (data.path ? [data.path] : [])) as string[]; + await handleMovePaths(paths, currentPath); } catch {} return; } @@ -834,7 +831,7 @@ } uploading = false; invalidateTreeCache(currentPath); - await loadDir(currentPath, { preserveTree: true }); + await loadDir(currentPath, { preserveTree: true }); }; const handleUploadFiles = async (files: File[]) => { @@ -926,19 +923,25 @@ }; // ── Move (drag-and-drop) ──────────────────────────────────────────── - const handleMove = async (source: string, destFolder: string) => { + const sourceParentPath = (source: string) => { + const cleanSource = normalizePath(source).replace(/\/$/, ''); + const index = cleanSource.lastIndexOf('/'); + return asDirectoryPath(index >= 0 ? cleanSource.slice(0, index + 1) : currentPath); + }; + + const moveOne = async (source: string, destFolder: string) => { const terminal = selectedTerminal; - if (!terminal || !currentWritable) return; + if (!terminal || !currentWritable) return false; - const cleanSource = source.replace(/\/$/, ''); + const cleanSource = normalizePath(source).replace(/\/$/, ''); const fileName = cleanSource.split('/').pop() ?? ''; - const destination = `${asDirectoryPath(destFolder)}${fileName}`; + const destination = joinPath(destFolder, fileName); - if (!fileName || cleanSource === destination) return; + if (!fileName || cleanSource === destination) return false; // Prevent moving a folder into itself or its own subtree const sourceDir = asDirectoryPath(cleanSource); - if (asDirectoryPath(destFolder).startsWith(sourceDir)) return; + if (asDirectoryPath(destFolder).startsWith(sourceDir)) return false; const result = await moveEntry( terminal.url, @@ -949,11 +952,27 @@ ); if ('error' in result) { toast.error(result.error); + return false; } else { toast.success($i18n.t('Moved {{name}}', { name: fileName })); + return true; + } + }; + + const refreshAfterMove = async (sources: string[], destFolder: string) => { + invalidateTreeCache(currentPath, destFolder, ...sources, ...sources.map(sourceParentPath)); + clearSelection(); + await refreshBrowser(); + }; + + const handleMovePaths = async (sources: string[], destFolder: string) => { + const movedSources: string[] = []; + for (const source of sources) { + if (await moveOne(source, destFolder)) movedSources.push(source); + } + if (movedSources.length > 0) { + await refreshAfterMove(movedSources, destFolder); } - invalidateTreeCache(currentPath, destFolder, cleanSource.substring(0, cleanSource.lastIndexOf('/') + 1)); - await loadDir(currentPath, { preserveTree: true }); }; // ── Rename ────────────────────────────────────────────────────────── @@ -1325,7 +1344,7 @@ onNewFile={startNewFile} onUploadFiles={handleUploadFiles} onDownloadDir={() => downloadFile(currentPath)} - onMove={handleMove} + onMove={handleMovePaths} onSort={toggleSort} onToggleHidden={toggleHidden} > @@ -1618,10 +1637,17 @@ selected={selectedEntries.has(entry.fullPath)} {selectionMode} selectedPaths={selectedEntries} - onOpen={(row) => openEntry({ ...row, fullPath: entry.fullPath, parentPath: entry.parentPath, depth: entry.depth, rowIndex: entry.rowIndex })} + onOpen={(row) => + openEntry({ + ...row, + fullPath: entry.fullPath, + parentPath: entry.parentPath, + depth: entry.depth, + rowIndex: entry.rowIndex + })} onDownload={downloadFile} onDelete={requestDelete} - onMove={handleMove} + onMove={handleMovePaths} onRename={handleRename} onSelect={handleSelect} onLongPress={enterSelectionMode} diff --git a/src/lib/components/chat/FileNav/FileEntryRow.svelte b/src/lib/components/chat/FileNav/FileEntryRow.svelte index 1ce55bf4b7..2aa511a301 100644 --- a/src/lib/components/chat/FileNav/FileEntryRow.svelte +++ b/src/lib/components/chat/FileNav/FileEntryRow.svelte @@ -24,7 +24,7 @@ export let onOpen: (entry: FileEntry) => void = () => {}; export let onDownload: (path: string) => void = () => {}; export let onDelete: (path: string, name: string) => void = () => {}; - export let onMove: (source: string, destFolder: string) => void = () => {}; + export let onMove: (sources: string[], destFolder: string) => void | Promise = () => {}; export let onRename: (oldPath: string, newName: string) => void = () => {}; // ── Selection ───────────────────────────────────────────────────────── @@ -187,7 +187,7 @@ dragOverFolder = false; clearExpandTimer(); }} - on:drop={(e) => { + on:drop={async (e) => { if (entry.type !== 'directory') return; if (!writable) return; const raw = e.dataTransfer?.getData('application/x-terminal-file-move'); @@ -198,12 +198,12 @@ clearExpandTimer(); try { const data = JSON.parse(raw); - const paths = data.paths || (data.path ? [data.path] : []); + const paths = (data.paths || (data.path ? [data.path] : [])) as string[]; const destFolder = directoryPath; - for (const p of paths) { - if (p + '/' === destFolder || p === destFolder) continue; - onMove(p, destFolder); - } + await onMove( + paths.filter((p) => p + '/' !== destFolder && p !== destFolder), + destFolder + ); } catch {} }} > diff --git a/src/lib/components/chat/FileNav/FileNavToolbar.svelte b/src/lib/components/chat/FileNav/FileNavToolbar.svelte index 1ff2dffc7b..5a1356ae3a 100644 --- a/src/lib/components/chat/FileNav/FileNavToolbar.svelte +++ b/src/lib/components/chat/FileNav/FileNavToolbar.svelte @@ -19,7 +19,7 @@ export let onNewFile: () => void = () => {}; export let onUploadFiles: (files: File[]) => void = () => {}; export let onDownloadDir: () => void = () => {}; - export let onMove: (source: string, destFolder: string) => void = () => {}; + export let onMove: (sources: string[], destFolder: string) => void | Promise = () => {}; export let showHidden = false; export let onToggleHidden: () => void = () => {}; @@ -50,7 +50,9 @@ }); -
+
@@ -108,7 +110,7 @@ on:dragleave={() => { if (dragOverCrumb === i) dragOverCrumb = null; }} - on:drop={(e) => { + on:drop={async (e) => { if (!writable) return; const raw = e.dataTransfer?.getData('application/x-terminal-file-move'); if (!raw) return; @@ -117,8 +119,8 @@ dragOverCrumb = null; try { const data = JSON.parse(raw); - const paths = data.paths || (data.path ? [data.path] : []); - for (const p of paths) onMove(p, crumb.path); + const paths = (data.paths || (data.path ? [data.path] : [])) as string[]; + await onMove(paths, crumb.path); } catch {} }} >