diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx
index 4fd75591..a6364308 100644
--- a/apps/x/apps/renderer/src/App.tsx
+++ b/apps/x/apps/renderer/src/App.tsx
@@ -17,7 +17,8 @@ import { ImageFileViewer } from '@/components/image-file-viewer';
import { VideoFileViewer } from '@/components/video-file-viewer';
import { AudioFileViewer } from '@/components/audio-file-viewer';
import { PersistentViewerCache } from '@/components/persistent-viewer-cache';
-import { getViewerType, isMediaPath, isCacheableViewerPath } from '@/lib/file-types';
+import { UnsupportedFileViewer } from '@/components/unsupported-file-viewer';
+import { getViewerType, isCacheableViewerPath } from '@/lib/file-types';
import { useDebounce } from './hooks/use-debounce';
import { SidebarContentPanel } from '@/components/sidebar-content';
import { SuggestedTopicsView } from '@/components/suggested-topics-view';
@@ -1429,10 +1430,10 @@ function App() {
}
const requestId = (fileLoadRequestIdRef.current += 1)
const pathToLoad = selectedPath
- // Media viewers (HTML, image, video, audio, PDF) self-load via app:// protocol.
- // Skip the generic UTF-8 loader so we don't trash fileContent with binary
- // bytes or double-fetch large files.
- if (isMediaPath(pathToLoad)) {
+ // Only the markdown editor still consumes fileContent. Every other viewer
+ // (media + UnsupportedFileViewer) self-loads, so skip the generic UTF-8
+ // loader to avoid double-fetching and to avoid slurping binary bytes.
+ if (!pathToLoad.endsWith('.md')) {
setFileContent('')
return
}
@@ -4853,10 +4854,8 @@ function App() {
) : (
-
-
- {fileContent || 'Loading...'}
-
+
+
)
)}
diff --git a/apps/x/apps/renderer/src/components/unsupported-file-viewer.tsx b/apps/x/apps/renderer/src/components/unsupported-file-viewer.tsx
new file mode 100644
index 00000000..474d7dba
--- /dev/null
+++ b/apps/x/apps/renderer/src/components/unsupported-file-viewer.tsx
@@ -0,0 +1,149 @@
+import { useEffect, useState } from 'react'
+import { ExternalLinkIcon, FileIcon, FileTextIcon, Loader2Icon } from 'lucide-react'
+
+const TEXT_FALLBACK_MAX_BYTES = 1 * 1024 * 1024 // 1 MB
+
+interface UnsupportedFileViewerProps {
+ path: string
+}
+
+type State =
+ | { kind: 'loading' }
+ | { kind: 'ready'; sizeBytes: number; canShowAsText: boolean }
+ | { kind: 'error'; message: string }
+
+function basename(path: string): string {
+ const idx = path.lastIndexOf('/')
+ return idx >= 0 ? path.slice(idx + 1) : path
+}
+
+function extensionLabel(path: string): string {
+ const name = basename(path)
+ const dot = name.lastIndexOf('.')
+ if (dot < 0) return 'No extension'
+ return name.slice(dot + 1).toUpperCase()
+}
+
+function formatSize(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
+}
+
+export function UnsupportedFileViewer({ path }: UnsupportedFileViewerProps) {
+ const [state, setState] = useState
({ kind: 'loading' })
+ const [textContent, setTextContent] = useState(null)
+
+ useEffect(() => {
+ let cancelled = false
+ setState({ kind: 'loading' })
+ setTextContent(null)
+
+ ;(async () => {
+ try {
+ const stat = await window.ipc.invoke('workspace:stat', { path })
+ if (cancelled) return
+ if (stat.kind !== 'file') {
+ setState({ kind: 'error', message: 'Selected path is not a file.' })
+ return
+ }
+ setState({
+ kind: 'ready',
+ sizeBytes: stat.size,
+ canShowAsText: stat.size <= TEXT_FALLBACK_MAX_BYTES,
+ })
+ } catch (err) {
+ if (cancelled) return
+ const message = err instanceof Error ? err.message : String(err)
+ setState({ kind: 'error', message })
+ }
+ })()
+
+ return () => {
+ cancelled = true
+ }
+ }, [path])
+
+ async function loadAsText() {
+ try {
+ const result = await window.ipc.invoke('workspace:readFile', { path })
+ setTextContent(result.data)
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err)
+ setTextContent(`Failed to read as text: ${message}`)
+ }
+ }
+
+ if (state.kind === 'loading') {
+ return (
+
+
+
+ )
+ }
+
+ if (state.kind === 'error') {
+ return (
+
+
+
Could not open
+
{state.message}
+
+ )
+ }
+
+ if (textContent !== null) {
+ return (
+
+
+ {basename(path)} · plain text view
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ {basename(path)}
+
+
+ {extensionLabel(path)} · {formatSize(state.sizeBytes)}
+
+
No in-app preview for this file type.
+
+
+ {state.canShowAsText && (
+
+ )}
+
+
+ )
+}