fix(files): prevent autosave data loss on load lag and binary files

Guard FilesView autosave until the selected file has finished loading,
refuse binary/PDF/office/archive text saves, and add a persisted global
autoSaveEnabled setting (default true) under Settings → General.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-07-30 06:37:47 +00:00
co-authored by Serhii Dziupin
parent a4c7bac303
commit 5b727ed53c
34 changed files with 504 additions and 55 deletions
+26 -4
View File
@@ -28,7 +28,7 @@ import { copyTextToClipboard } from '@/lib/clipboard';
import { useI18n } from '@/lib/i18n';
import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
import { getDefaultTheme } from '@/lib/theme/themes';
import { getImageMimeType, getLanguageFromExtension, isImageFile } from '@/lib/toolHelpers';
import { getImageMimeType, getLanguageFromExtension, isBinaryFile, isImageFile, isPdfFile, isSvgFile, looksLikeBinaryText } from '@/lib/toolHelpers';
import type { FileListEntry, FileSearchResult } from '@/lib/api/types';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { cn } from '@/lib/utils';
@@ -98,6 +98,7 @@ export const MobileFilesSurface: React.FC<MobileFilesSurfaceProps> = ({ onClose
const [imageSrc, setImageSrc] = React.useState('');
const [fileError, setFileError] = React.useState<string | null>(null);
const [isLoadingFile, setIsLoadingFile] = React.useState(false);
const [binaryBlocked, setBinaryBlocked] = React.useState(false);
const directoryLoadRequestIdRef = React.useRef(0);
React.useEffect(() => {
@@ -174,8 +175,9 @@ export const MobileFilesSurface: React.FC<MobileFilesSurfaceProps> = ({ onClose
setFileContent('');
setImageSrc('');
setFileError(null);
setBinaryBlocked(false);
if (isImageFile(route.path) && !route.path.toLowerCase().endsWith('.svg')) {
if (isImageFile(route.path) && !isSvgFile(route.path)) {
let cancelled = false;
let objectUrl = '';
setIsLoadingFile(true);
@@ -203,6 +205,14 @@ export const MobileFilesSurface: React.FC<MobileFilesSurfaceProps> = ({ onClose
};
}
// Never load PDF/office/archives/etc. as UTF-8 text — that path can corrupt originals
// if a future write path is added, and it shows gibberish in the viewer.
if (isBinaryFile(route.path) || isPdfFile(route.path)) {
setBinaryBlocked(true);
setIsLoadingFile(false);
return;
}
if (!files.readFile) {
setFileError(t('mobile.files.error.readUnavailable'));
setIsLoadingFile(false);
@@ -214,6 +224,11 @@ export const MobileFilesSurface: React.FC<MobileFilesSurfaceProps> = ({ onClose
void files.readFile(route.path)
.then((result) => {
if (cancelled) return;
if (looksLikeBinaryText(result.content)) {
setBinaryBlocked(true);
setFileContent('');
return;
}
setFileContent(result.content.length > MAX_MOBILE_FILE_CHARS
? `${result.content.slice(0, MAX_MOBILE_FILE_CHARS)}\n\n${t('mobile.files.file.truncated')}`
: result.content);
@@ -263,6 +278,7 @@ export const MobileFilesSurface: React.FC<MobileFilesSurfaceProps> = ({ onClose
imageSrc={imageSrc}
error={fileError}
isLoading={isLoadingFile}
binaryBlocked={binaryBlocked}
onBack={() => setRoute({ type: 'browser', directory: route.returnDirectory })}
onCopyPath={() => void handleCopyPath(route.path)}
onCopyContent={() => void handleCopyContent()}
@@ -413,10 +429,11 @@ const MobileFileDetail: React.FC<{
imageSrc: string;
error: string | null;
isLoading: boolean;
binaryBlocked: boolean;
onBack: () => void;
onCopyPath: () => void;
onCopyContent: () => void;
}> = ({ path, content, imageSrc, error, isLoading, onBack, onCopyPath, onCopyContent }) => {
}> = ({ path, content, imageSrc, error, isLoading, binaryBlocked, onBack, onCopyPath, onCopyContent }) => {
const { t } = useI18n();
return (
@@ -433,7 +450,7 @@ const MobileFileDetail: React.FC<{
<div className="min-w-0 flex-1">
<h2 className="truncate typography-ui-header text-foreground">{getNameFromPath(path)}</h2>
</div>
{!isImageFile(path) ? (
{!isImageFile(path) && !binaryBlocked ? (
<Button type="button" variant="ghost" size="icon" onClick={onCopyContent} aria-label={t('mobile.files.copyContentAria')}>
<RiFileCopyLine className="size-4" />
</Button>
@@ -455,6 +472,11 @@ const MobileFileDetail: React.FC<{
<ScrollShadow className="h-full overflow-auto p-4">
<img src={`data:${getImageMimeType(path)};utf8,${encodeURIComponent(content)}`} alt={getNameFromPath(path)} className="mx-auto max-h-full max-w-full rounded-lg object-contain" />
</ScrollShadow>
) : binaryBlocked ? (
<div className="flex h-full flex-col items-center justify-center gap-2 p-6 text-center">
<div className="typography-ui-header text-foreground">{t('filesView.editor.cannotPreviewBinary')}</div>
<div className="max-w-sm typography-ui text-muted-foreground">{t('filesView.editor.binaryFileDescription')}</div>
</div>
) : (
<MobileTextFile path={path} content={content} />
)}