From fd8972a7d93d935e430872731235b16f16bfbabb Mon Sep 17 00:00:00 2001 From: Dave Otero <31802309+daveotero@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:33:55 -0600 Subject: [PATCH] Add save actions and cross-platform file manager support (#848) * files-download-feature * feat: add save option to file directory context menus and viewer * fix: open files in the system file manager --- .../components/layout/SidebarFilesTree.tsx | 16 ++++++++-- .../ui/src/components/views/FilesView.tsx | 32 +++++++++++++++++-- packages/ui/src/lib/api/types.ts | 1 + packages/ui/src/lib/utils.ts | 11 +++++++ packages/vscode/webview/api/files.ts | 12 ++++++- packages/web/server/lib/fs/routes.js | 25 ++++++++++++++- packages/web/src/api/files.ts | 11 +++++++ 7 files changed, 102 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/layout/SidebarFilesTree.tsx b/packages/ui/src/components/layout/SidebarFilesTree.tsx index 47121de3..24086af5 100644 --- a/packages/ui/src/components/layout/SidebarFilesTree.tsx +++ b/packages/ui/src/components/layout/SidebarFilesTree.tsx @@ -13,6 +13,7 @@ import { RiMore2Fill, RiRefreshLine, RiSearchLine, + RiDownloadLine, } from '@remixicon/react'; import { toast } from '@/components/ui'; @@ -44,7 +45,7 @@ import { useGitStatus } from '@/stores/useGitStore'; import { useDirectoryShowHidden } from '@/lib/directoryShowHidden'; import { useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored'; import { copyTextToClipboard } from '@/lib/clipboard'; -import { cn } from '@/lib/utils'; +import { cn, getRevealLabel } from '@/lib/utils'; import { opencodeClient } from '@/lib/opencode/client'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard'; @@ -132,6 +133,7 @@ interface FileRowProps { canDelete: boolean; canReveal: boolean; }; + downloadFile?: (path: string) => Promise; contextMenuPath: string | null; setContextMenuPath: (path: string | null) => void; onSelect: (node: FileNode) => void; @@ -147,6 +149,7 @@ const FileRow: React.FC = ({ status, badge, permissions, + downloadFile, contextMenuPath, setContextMenuPath, onSelect, @@ -244,9 +247,17 @@ const FileRow: React.FC = ({ }}> Copy Path + {!isDir && downloadFile && ( + { + e.stopPropagation(); + void downloadFile(node.path); + }}> + Save + + )} {canReveal && ( { e.stopPropagation(); onRevealPath(node.path); }}> - Reveal in Finder + {getRevealLabel()} )} {isDir && (canCreateFile || canCreateFolder) && ( @@ -749,6 +760,7 @@ export const SidebarFilesTree: React.FC = () => { status={!isDir ? getFileStatus(node.path) : undefined} badge={isDir ? getFolderBadge(node.path) : undefined} permissions={{ canRename, canCreateFile, canCreateFolder, canDelete, canReveal }} + downloadFile={files.downloadFile} contextMenuPath={contextMenuPath} setContextMenuPath={setContextMenuPath} onSelect={handleOpenFile} diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 894d3d00..6aeadce9 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -26,6 +26,7 @@ import { RiFileTransferLine, RiCodeSSlashLine, RiNodeTree, + RiDownloadLine, } from '@remixicon/react'; import { toast } from '@/components/ui'; import { copyTextToClipboard } from '@/lib/clipboard'; @@ -58,7 +59,7 @@ import { import { useDebouncedValue } from '@/hooks/useDebouncedValue'; import { useFileSearchStore } from '@/stores/useFileSearchStore'; import { useDeviceInfo } from '@/lib/device'; -import { cn, getModifierLabel, hasModifier } from '@/lib/utils'; +import { cn, getModifierLabel, getRevealLabel, hasModifier } from '@/lib/utils'; import { getLanguageFromExtension, getImageMimeType, isImageFile } from '@/lib/toolHelpers'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { EditorView } from '@codemirror/view'; @@ -292,6 +293,7 @@ interface FileRowProps { canDelete: boolean; canReveal: boolean; }; + downloadFile?: (path: string) => Promise; contextMenuPath: string | null; setContextMenuPath: (path: string | null) => void; onSelect: (node: FileNode) => void; @@ -308,6 +310,7 @@ const FileRow: React.FC = ({ status, badge, permissions, + downloadFile, contextMenuPath, setContextMenuPath, onSelect, @@ -414,9 +417,17 @@ const FileRow: React.FC = ({ }}> Copy Path + {!isDir && downloadFile && ( + { + e.stopPropagation(); + void downloadFile(node.path); + }}> + Save + + )} {canReveal && ( { e.stopPropagation(); onRevealPath(node.path); }}> - Reveal in Finder + {getRevealLabel()} )} {isDir && (canCreateFile || canCreateFolder) && ( @@ -1638,6 +1649,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { status={!isDir ? getFileStatus(node.path) : undefined} badge={isDir ? getFolderBadge(node.path) : undefined} permissions={{ canRename, canCreateFile, canCreateFolder, canDelete, canReveal }} + downloadFile={files.downloadFile} contextMenuPath={contextMenuPath} setContextMenuPath={setContextMenuPath} onSelect={handleSelectFile} @@ -2426,6 +2438,22 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { )} + {files.downloadFile && ( + + )} + {exitFullscreenOnly ? (