From 3841e85abb3ea3e8d8b364dff0102f0124844d22 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 24 Mar 2026 05:34:36 -0500 Subject: [PATCH] refac --- src/lib/apis/terminal/index.ts | 43 ++++++++++++++++ src/lib/components/chat/FileNav.svelte | 28 +++++++++-- .../chat/FileNav/BulkActionBar.svelte | 42 ++++++++-------- .../chat/FileNav/FileEntryRow.svelte | 49 ++++++++++--------- 4 files changed, 112 insertions(+), 50 deletions(-) diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts index 748ada33a0..828931045a 100644 --- a/src/lib/apis/terminal/index.ts +++ b/src/lib/apis/terminal/index.ts @@ -120,6 +120,49 @@ export const downloadFileBlob = async ( return { blob, filename }; }; +export const downloadDirectoryArchive = async ( + baseUrl: string, + apiKey: string, + path: string +): Promise<{ blob: Blob; filename: string } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/download?path=${encodeURIComponent(path)}`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}` } + }).catch(() => null); + + if (!res || !res.ok) return null; + + const disposition = res.headers.get('content-disposition') ?? ''; + const match = disposition.match(/filename="?([^"]+)"?/); + const filename = match?.[1] ?? `${path.split('/').filter(Boolean).pop() ?? 'download'}.zip`; + const blob = await res.blob(); + return { blob, filename }; +}; + +export const downloadMultipleAsZip = async ( + baseUrl: string, + apiKey: string, + paths: string[] +): Promise<{ blob: Blob; filename: string } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/download`; + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ paths }) + }).catch(() => null); + + if (!res || !res.ok) return null; + + const disposition = res.headers.get('content-disposition') ?? ''; + const match = disposition.match(/filename="?([^"]+)"?/); + const filename = match?.[1] ?? 'download.zip'; + const blob = await res.blob(); + return { blob, filename }; +}; + export const uploadToTerminal = async ( baseUrl: string, apiKey: string, diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index 576371c160..c41038393f 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -19,6 +19,8 @@ listFiles, readFile, downloadFileBlob, + downloadDirectoryArchive, + downloadMultipleAsZip, uploadToTerminal, createDirectory, deleteEntry, @@ -403,7 +405,11 @@ const terminal = selectedTerminal; if (!terminal) return; - const result = await downloadFileBlob(terminal.url, terminal.key, path); + // Directories end with '/' — download as ZIP archive + const isDir = path.endsWith('/'); + const result = isDir + ? await downloadDirectoryArchive(terminal.url, terminal.key, path.replace(/\/$/, '')) + : await downloadFileBlob(terminal.url, terminal.key, path); if (!result) return; const url = URL.createObjectURL(result.blob); const a = document.createElement('a'); @@ -644,10 +650,24 @@ const terminal = selectedTerminal; if (!terminal) return; - const filePaths = [...selectedEntries].filter((p) => !p.endsWith('/')); - for (const p of filePaths) { - await downloadFile(p); + const paths = [...selectedEntries].map((p) => p.replace(/\/$/, '')); + if (paths.length === 0) return; + + // Single item — use the regular downloadFile path + if (paths.length === 1) { + await downloadFile([...selectedEntries][0]); + return; } + + // Multiple items — bundle into a single ZIP + const result = await downloadMultipleAsZip(terminal.url, terminal.key, paths); + if (!result) return; + const url = URL.createObjectURL(result.blob); + const a = document.createElement('a'); + a.href = url; + a.download = result.filename; + a.click(); + URL.revokeObjectURL(url); }; // Escape to clear selection diff --git a/src/lib/components/chat/FileNav/BulkActionBar.svelte b/src/lib/components/chat/FileNav/BulkActionBar.svelte index 405b529562..9652f9ad49 100644 --- a/src/lib/components/chat/FileNav/BulkActionBar.svelte +++ b/src/lib/components/chat/FileNav/BulkActionBar.svelte @@ -40,29 +40,27 @@ - {#if hasFiles} - - - - {/if} + + + + + - {/if} + + + +
{$i18n.t('Download')}
+