diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json
index 64febcdf17..3ccb4abb2e 100644
--- a/frontend/src-tauri/capabilities/default.json
+++ b/frontend/src-tauri/capabilities/default.json
@@ -44,8 +44,13 @@
"identifier": "fs:allow-write-file",
"allow": [{ "path": "**" }]
},
+ {
+ "identifier": "fs:allow-remove",
+ "allow": [{ "path": "**" }]
+ },
"dialog:default",
"dialog:allow-open",
+ "dialog:allow-save",
"opener:default",
"shell:allow-open"
]
diff --git a/frontend/src/core/components/shared/RightRail.tsx b/frontend/src/core/components/shared/RightRail.tsx
index 7148b4c3bc..4e17210d75 100644
--- a/frontend/src/core/components/shared/RightRail.tsx
+++ b/frontend/src/core/components/shared/RightRail.tsx
@@ -22,6 +22,7 @@ import LightModeIcon from '@mui/icons-material/LightMode';
import { useSidebarContext } from '@app/contexts/SidebarContext';
import { RightRailButtonConfig, RightRailRenderContext, RightRailSection } from '@app/types/rightRail';
import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide';
+import { isTauri } from '@tauri-apps/api/core';
const SECTION_ORDER: RightRailSection[] = ['top', 'middle', 'bottom'];
@@ -151,16 +152,51 @@ export default function RightRail() {
return;
}
- const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles;
- filesToDownload.forEach(file => {
- const link = document.createElement('a');
- link.href = URL.createObjectURL(file);
- link.download = file.name;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- URL.revokeObjectURL(link.href);
- });
+ const filesToExport = selectedFiles.length > 0 ? selectedFiles : activeFiles;
+
+ // Desktop mode: Show "Save As" dialog for each file
+ if (isTauri()) {
+ try {
+ // @ts-ignore - Desktop module not available in proprietary build
+ const { showSaveDialog, saveToLocalPath } = await import('@desktop/services/localFileSaveService');
+
+ for (const file of filesToExport) {
+ const savePath = await showSaveDialog(file.name);
+ if (savePath) {
+ const result = await saveToLocalPath(file, savePath);
+ if (result.success) {
+ console.log(`[RightRail] Saved to: ${savePath}`);
+ } else {
+ console.error(`[RightRail] Failed to save: ${result.error}`);
+ alert(`Failed to save ${file.name}: ${result.error}`);
+ }
+ }
+ }
+ } catch (error) {
+ console.error('[RightRail] Save As error:', error);
+ // Fallback to browser download
+ filesToExport.forEach(file => {
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(file);
+ link.download = file.name;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(link.href);
+ });
+ }
+ } else {
+ // Web mode: Regular download
+ filesToExport.forEach(file => {
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(file);
+ link.download = file.name;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(link.href);
+ });
+ }
}, [
currentView,
selectedFiles,
@@ -174,11 +210,21 @@ export default function RightRail() {
if (currentView === 'pageEditor') {
return t('rightRail.exportAll', 'Export PDF');
}
+
+ // Desktop mode: "Save As" instead of "Download"
+ if (isTauri()) {
+ if (selectedCount > 0) {
+ return selectedCount === 1 ? 'Save As...' : `Save ${selectedCount} Files As...`;
+ }
+ return 'Save As...';
+ }
+
+ // Web mode: "Download"
if (selectedCount > 0) {
return terminology.downloadSelected;
}
return terminology.downloadAll;
- }, [currentView, selectedCount, t]);
+ }, [currentView, selectedCount, t, terminology]);
return (
diff --git a/frontend/src/core/contexts/FileContext.tsx b/frontend/src/core/contexts/FileContext.tsx
index f63ae0f694..2c85163183 100644
--- a/frontend/src/core/contexts/FileContext.tsx
+++ b/frontend/src/core/contexts/FileContext.tsx
@@ -328,6 +328,47 @@ function FileContextInner({
addFiles: addRawFiles,
addStirlingFileStubs: addStirlingFileStubsAction,
removeFiles: async (fileIds: FileId[], deleteFromStorage?: boolean) => {
+ // Check if any files have localFilePath (desktop mode)
+ const filesWithLocalPaths: Array<{ id: FileId; path: string; name: string }> = [];
+ for (const fileId of fileIds) {
+ const stub = stateRef.current.files.byId[fileId];
+ if (stub?.localFilePath) {
+ filesWithLocalPaths.push({
+ id: fileId,
+ path: stub.localFilePath,
+ name: stub.name
+ });
+ }
+ }
+
+ // Ask user if they want to delete from disk (desktop only)
+ if (filesWithLocalPaths.length > 0) {
+ const fileList = filesWithLocalPaths.map(f => `• ${f.name}`).join('\n');
+ const message = filesWithLocalPaths.length === 1
+ ? `Delete "${filesWithLocalPaths[0].name}" from disk?\n\nThis will permanently delete the file from:\n${filesWithLocalPaths[0].path}`
+ : `Delete ${filesWithLocalPaths.length} files from disk?\n\n${fileList}\n\nThis will permanently delete these files from your computer.`;
+
+ const shouldDeleteFromDisk = window.confirm(message);
+
+ if (shouldDeleteFromDisk) {
+ try {
+ // @ts-ignore - Desktop module not available in proprietary build
+ const { deleteLocalFile } = await import('@desktop/services/localFileSaveService');
+
+ for (const file of filesWithLocalPaths) {
+ const result = await deleteLocalFile(file.path);
+ if (result.success) {
+ console.log(`[FileContext] Deleted from disk: ${file.name}`);
+ } else {
+ console.warn(`[FileContext] Failed to delete from disk: ${file.name}`, result.error);
+ }
+ }
+ } catch (error) {
+ console.error('[FileContext] Failed to delete files from disk:', error);
+ }
+ }
+ }
+
// Remove from memory and cleanup resources
lifecycleManager.removeFiles(fileIds, stateRef);
diff --git a/frontend/src/desktop/services/localFileSaveService.ts b/frontend/src/desktop/services/localFileSaveService.ts
index 527b8e82c3..7e44ab216f 100644
--- a/frontend/src/desktop/services/localFileSaveService.ts
+++ b/frontend/src/desktop/services/localFileSaveService.ts
@@ -43,6 +43,63 @@ export function shouldAutoSave(inputCount: number, outputCount: number): boolean
return isTauri() && inputCount === 1 && outputCount === 1;
}
+/**
+ * Delete a file from local filesystem (Tauri desktop only)
+ *
+ * @param filePath - Absolute path to delete
+ * @returns Result indicating success or failure
+ */
+export async function deleteLocalFile(filePath: string): Promise {
+ if (!isTauri()) {
+ return { success: false, error: "Not running in Tauri desktop app" };
+ }
+
+ try {
+ const { remove } = await import("@tauri-apps/plugin-fs");
+ await remove(filePath);
+ console.log(`[LocalFileDelete] Deleted: ${filePath}`);
+ return { success: true };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ console.error('[LocalFileDelete] Failed to delete:', message);
+ return { success: false, error: message };
+ }
+}
+
+/**
+ * Show native save dialog and return selected path
+ *
+ * @param defaultFilename - Suggested filename
+ * @param defaultDirectory - Optional default directory
+ * @returns Selected file path or null if cancelled
+ */
+export async function showSaveDialog(
+ defaultFilename: string,
+ defaultDirectory?: string
+): Promise {
+ if (!isTauri()) {
+ return null;
+ }
+
+ try {
+ const { save } = await import("@tauri-apps/plugin-dialog");
+
+ const selectedPath = await save({
+ defaultPath: defaultDirectory ? `${defaultDirectory}/${defaultFilename}` : defaultFilename,
+ filters: [{
+ name: 'PDF',
+ extensions: ['pdf']
+ }],
+ title: 'Save As'
+ });
+
+ return selectedPath;
+ } catch (error) {
+ console.error('[SaveDialog] Failed to show dialog:', error);
+ return null;
+ }
+}
+
export interface MultiFileSaveResult {
success: boolean;
savedCount: number;