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
This commit is contained in:
Dave Otero
2026-04-16 23:33:55 +03:00
committed by GitHub
parent 79a9f93ae2
commit fd8972a7d9
7 changed files with 102 additions and 6 deletions
@@ -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<void>;
contextMenuPath: string | null;
setContextMenuPath: (path: string | null) => void;
onSelect: (node: FileNode) => void;
@@ -147,6 +149,7 @@ const FileRow: React.FC<FileRowProps> = ({
status,
badge,
permissions,
downloadFile,
contextMenuPath,
setContextMenuPath,
onSelect,
@@ -244,9 +247,17 @@ const FileRow: React.FC<FileRowProps> = ({
}}>
<RiFileCopyLine className="mr-2 h-4 w-4" /> Copy Path
</DropdownMenuItem>
{!isDir && downloadFile && (
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
void downloadFile(node.path);
}}>
<RiDownloadLine className="mr-2 h-4 w-4" /> Save
</DropdownMenuItem>
)}
{canReveal && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onRevealPath(node.path); }}>
<RiFolderReceivedLine className="mr-2 h-4 w-4" /> Reveal in Finder
<RiFolderReceivedLine className="mr-2 h-4 w-4" /> {getRevealLabel()}
</DropdownMenuItem>
)}
{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}
+30 -2
View File
@@ -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<void>;
contextMenuPath: string | null;
setContextMenuPath: (path: string | null) => void;
onSelect: (node: FileNode) => void;
@@ -308,6 +310,7 @@ const FileRow: React.FC<FileRowProps> = ({
status,
badge,
permissions,
downloadFile,
contextMenuPath,
setContextMenuPath,
onSelect,
@@ -414,9 +417,17 @@ const FileRow: React.FC<FileRowProps> = ({
}}>
<RiFileCopyLine className="mr-2 h-4 w-4" /> Copy Path
</DropdownMenuItem>
{!isDir && downloadFile && (
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
void downloadFile(node.path);
}}>
<RiDownloadLine className="mr-2 h-4 w-4" /> Save
</DropdownMenuItem>
)}
{canReveal && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onRevealPath(node.path); }}>
<RiFolderReceivedLine className="mr-2 h-4 w-4" /> Reveal in Finder
<RiFolderReceivedLine className="mr-2 h-4 w-4" /> {getRevealLabel()}
</DropdownMenuItem>
)}
{isDir && (canCreateFile || canCreateFolder) && (
@@ -1638,6 +1649,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ 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<FilesViewProps> = ({ mode = 'full' }) => {
</Button>
)}
{files.downloadFile && (
<Button
variant="ghost"
size="sm"
onClick={() => {
const fn = files.downloadFile;
if (fn) void fn(selectedFile.path);
}}
className="h-6 w-6 p-0"
title="Save file"
aria-label="Save file"
>
<RiDownloadLine className="h-4 w-4" />
</Button>
)}
{exitFullscreenOnly ? (
<Button
variant="ghost"