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 entry.type !== 'directory'}
- {
- e.stopPropagation();
- onDownload(`${currentPath}${entry.name}`);
- }}
+ {
+ e.stopPropagation();
+ const path = entry.type === 'directory'
+ ? `${currentPath}${entry.name}/`
+ : `${currentPath}${entry.name}`;
+ onDownload(path);
+ }}
+ >
+
-
-
-
-
- {$i18n.t('Download')}
-
- {/if}
+
+
+
+ {$i18n.t('Download')}
+