diff --git a/src/components/code-editor/hooks/useCodeEditorDocument.ts b/src/components/code-editor/hooks/useCodeEditorDocument.ts
index 74f928d6..dda02887 100644
--- a/src/components/code-editor/hooks/useCodeEditorDocument.ts
+++ b/src/components/code-editor/hooks/useCodeEditorDocument.ts
@@ -44,13 +44,17 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume
// Natively previewable media (image/pdf/audio/video) is rendered by
// CodeEditorMediaPreview, so there is nothing to read as text here.
+ // Clear any buffer left over from a previously opened text file so a
+ // stray save can't write stale content over the binary file.
if (getPreviewKind(file.name)) {
+ setContent('');
setLoading(false);
return;
}
// Check if file is binary by extension
if (isBinaryFile(file.name)) {
+ setContent('');
setIsBinary(true);
setLoading(false);
return;
@@ -87,6 +91,12 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume
}, [file.diffInfo, file.name, fileDiffNewString, fileDiffOldString, fileName, filePath, fileProjectId]);
const handleSave = useCallback(async () => {
+ // Preview-only and binary files have no editable text buffer; never write
+ // them back (e.g. via Cmd/Ctrl+S) or we'd corrupt the file on disk.
+ if (previewKind || isBinaryFile(fileName)) {
+ return;
+ }
+
setSaving(true);
setSaveError(null);
@@ -120,7 +130,7 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume
} finally {
setSaving(false);
}
- }, [content, filePath, fileProjectId]);
+ }, [content, filePath, fileProjectId, previewKind, fileName]);
const handleDownload = useCallback(() => {
const blob = new Blob([content], { type: 'text/plain' });
diff --git a/src/components/code-editor/utils/previewableFile.ts b/src/components/code-editor/utils/previewableFile.ts
index 464cad81..0071de4e 100644
--- a/src/components/code-editor/utils/previewableFile.ts
+++ b/src/components/code-editor/utils/previewableFile.ts
@@ -5,27 +5,59 @@
export type PreviewKind = 'image' | 'pdf' | 'video' | 'audio';
-// Formats browsers can decode in
.
-const IMAGE_EXTENSIONS = new Set([
- 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp', 'avif', 'apng',
-]);
+// Single source of truth: every extension the browser can preview, mapped to the
+// MIME type we apply when the server response has a missing/generic Content-Type.
+// The preview kind is derived from the MIME type so the two never drift apart.
+// Formats browsers generally can't play (avi, mkv, flv, wmv) are intentionally
+// absent and keep the binary fallback.
+const EXTENSION_MIME: Record = {
+ // Images
+ png: 'image/png',
+ jpg: 'image/jpeg',
+ jpeg: 'image/jpeg',
+ gif: 'image/gif',
+ svg: 'image/svg+xml',
+ webp: 'image/webp',
+ ico: 'image/x-icon',
+ bmp: 'image/bmp',
+ avif: 'image/avif',
+ apng: 'image/apng',
+ // PDF
+ pdf: 'application/pdf',
+ // Video
+ mp4: 'video/mp4',
+ webm: 'video/webm',
+ ogv: 'video/ogg',
+ mov: 'video/quicktime',
+ m4v: 'video/x-m4v',
+ // Audio
+ mp3: 'audio/mpeg',
+ wav: 'audio/wav',
+ m4a: 'audio/mp4',
+ aac: 'audio/aac',
+ flac: 'audio/flac',
+ opus: 'audio/opus',
+ oga: 'audio/ogg',
+ ogg: 'audio/ogg',
+ weba: 'audio/webm',
+};
-const PDF_EXTENSIONS = new Set(['pdf']);
+const extensionOf = (filename: string): string => filename.split('.').pop()?.toLowerCase() ?? '';
-// Container/codec combos broadly playable in