diff --git a/src/components/Folder/index.tsx b/src/components/Folder/index.tsx
index af8742643..a160eb641 100644
--- a/src/components/Folder/index.tsx
+++ b/src/components/Folder/index.tsx
@@ -41,6 +41,68 @@ import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { ZoomControls } from './ZoomControls';
+const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'];
+const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'];
+const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi', 'mkv', 'flv', 'wmv'];
+
+type FileTypeTarget = {
+ name?: string;
+ path?: string;
+ type?: string;
+};
+const loggedFileTypeWarnings = new Set();
+
+function getExt(value?: string) {
+ if (!value) return '';
+ const normalized = value.split(/[?#]/)[0];
+ const lastSegment = normalized.split('/').pop() || normalized;
+ if (!lastSegment.includes('.')) return '';
+ return lastSegment.split('.').pop()?.toLowerCase() || '';
+}
+
+function getFileType(file: FileTypeTarget) {
+ const extFromNameOrPath = getExt(file.name) || getExt(file.path);
+ const normalizedType = (file.type || '').replace(/^\./, '').toLowerCase();
+ const fileId = file.path || file.name || 'unknown-file';
+
+ if (!extFromNameOrPath && normalizedType) {
+ const key = `missing-ext|${fileId}|${normalizedType}`;
+ if (!loggedFileTypeWarnings.has(key)) {
+ loggedFileTypeWarnings.add(key);
+ console.warn(
+ `[Folder getFileType] extension missing in name/path, file.type fallback disabled: ${fileId} (type=${normalizedType})`
+ );
+ }
+ }
+
+ if (
+ extFromNameOrPath &&
+ normalizedType &&
+ normalizedType !== 'folder' &&
+ extFromNameOrPath !== normalizedType
+ ) {
+ const key = `mismatch|${fileId}|${extFromNameOrPath}|${normalizedType}`;
+ if (!loggedFileTypeWarnings.has(key)) {
+ loggedFileTypeWarnings.add(key);
+ console.warn(
+ `[Folder getFileType] extension/type mismatch for ${fileId}: inferred=${extFromNameOrPath}, type=${normalizedType}`
+ );
+ }
+ }
+
+ return extFromNameOrPath;
+}
+
+function isImageFile(file: FileTypeTarget) {
+ return IMAGE_EXTENSIONS.includes(getFileType(file));
+}
+function isAudioFile(file: FileTypeTarget) {
+ return AUDIO_EXTENSIONS.includes(getFileType(file));
+}
+function isVideoFile(file: FileTypeTarget) {
+ return VIDEO_EXTENSIONS.includes(getFileType(file));
+}
+
// Type definitions
interface FileTreeNode {
name: string;
@@ -244,6 +306,14 @@ export default function Folder({ data: _data }: { data?: Agent }) {
return;
}
+ // For audio/video files, skip open-file — loaders handle reading themselves
+ if (isAudioFile(file) || isVideoFile(file)) {
+ setSelectedFile({ ...file });
+ chatStore.setSelectedFile(chatStore.activeTaskId as string, file);
+ setLoading(false);
+ return;
+ }
+
// all other files call open-file interface, the backend handles download and parsing
window.ipcRenderer
.invoke('open-file', file.type, file.path, isShowSourceCode)
@@ -657,15 +727,15 @@ export default function Folder({ data: _data }: { data?: Agent }) {
- ) : [
- 'png',
- 'jpg',
- 'jpeg',
- 'gif',
- 'bmp',
- 'webp',
- 'svg',
- ].includes(selectedFile.type.toLowerCase()) ? (
+ ) : isAudioFile(selectedFile) ? (
+
+ ) : isVideoFile(selectedFile) ? (
+
+
+
+ ) : isImageFile(selectedFile) ? (
@@ -724,6 +794,75 @@ function ImageLoader({ selectedFile }: { selectedFile: FileInfo }) {
);
}
+function AudioLoader({ selectedFile }: { selectedFile: FileInfo }) {
+ const [src, setSrc] = useState('');
+
+ useEffect(() => {
+ let cancelled = false;
+ setSrc('');
+ if (selectedFile.isRemote) {
+ setSrc(selectedFile.content || selectedFile.path);
+ return;
+ }
+ window.electronAPI
+ .readFileAsDataUrl(selectedFile.path)
+ .then((dataUrl: string) => {
+ if (!cancelled) setSrc(dataUrl);
+ })
+ .catch((err: any) => {
+ if (cancelled) return;
+ console.error('Audio load error:', err);
+ setSrc('');
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [selectedFile]);
+
+ return (
+
+
+ {selectedFile.name}
+
+
+
+ );
+}
+
+function VideoLoader({ selectedFile }: { selectedFile: FileInfo }) {
+ const [src, setSrc] = useState('');
+
+ useEffect(() => {
+ let cancelled = false;
+ setSrc('');
+ if (selectedFile.isRemote) {
+ setSrc(selectedFile.content || selectedFile.path);
+ return;
+ }
+ window.electronAPI
+ .readFileAsDataUrl(selectedFile.path)
+ .then((dataUrl: string) => {
+ if (!cancelled) setSrc(dataUrl);
+ })
+ .catch((err: any) => {
+ if (cancelled) return;
+ console.error('Video load error:', err);
+ setSrc('');
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [selectedFile]);
+
+ return (
+
+ );
+}
+
// Helper function to get directory path from file path
function getDirPath(filePath: string): string {
const normalizedPath = filePath.replace(/\\/g, '/');