diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx
index 96ae3742..057aacb8 100644
--- a/apps/x/apps/renderer/src/App.tsx
+++ b/apps/x/apps/renderer/src/App.tsx
@@ -1425,10 +1425,11 @@ function App() {
}
const requestId = (fileLoadRequestIdRef.current += 1)
const pathToLoad = selectedPath
- // For HTML files, clear stale content immediately so the viewer shows
- // its loading state instead of rendering the previous file's bytes.
+ // HtmlFileViewer self-loads (with size check, error states, etc.)
+ // Skip the generic loader so we don't double-fetch large files.
if (pathToLoad.toLowerCase().endsWith('.html') || pathToLoad.toLowerCase().endsWith('.htm')) {
setFileContent('')
+ return
}
let cancelled = false
;(async () => {
@@ -4827,7 +4828,7 @@ function App() {
) : selectedPath?.toLowerCase().endsWith('.html') || selectedPath?.toLowerCase().endsWith('.htm') ? (
-
+
) : (
diff --git a/apps/x/apps/renderer/src/components/html-file-viewer.tsx b/apps/x/apps/renderer/src/components/html-file-viewer.tsx
index 3aceb117..41e7326c 100644
--- a/apps/x/apps/renderer/src/components/html-file-viewer.tsx
+++ b/apps/x/apps/renderer/src/components/html-file-viewer.tsx
@@ -1,33 +1,114 @@
import { useEffect, useState } from 'react'
-import { Loader2Icon } from 'lucide-react'
+import { AlertCircleIcon, ExternalLinkIcon, FileTextIcon, Loader2Icon } from 'lucide-react'
+
+const MAX_SIZE_BYTES = 5 * 1024 * 1024
+
+type ViewerState =
+ | { kind: 'loading' }
+ | { kind: 'loaded'; html: string }
+ | { kind: 'empty' }
+ | { kind: 'tooLarge'; sizeMB: number }
+ | { kind: 'error'; message: string }
interface HtmlFileViewerProps {
- html: string
path: string
}
-export function HtmlFileViewer({ html, path }: HtmlFileViewerProps) {
+export function HtmlFileViewer({ path }: HtmlFileViewerProps) {
+ const [state, setState] = useState
({ kind: 'loading' })
const [iframeLoaded, setIframeLoaded] = useState(false)
useEffect(() => {
+ let cancelled = false
+ setState({ kind: 'loading' })
setIframeLoaded(false)
- }, [path, html])
- const showSpinner = !html || !iframeLoaded
+ ;(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
+ }
+ if (stat.size > MAX_SIZE_BYTES) {
+ setState({ kind: 'tooLarge', sizeMB: stat.size / (1024 * 1024) })
+ return
+ }
+ const result = await window.ipc.invoke('workspace:readFile', { path })
+ if (cancelled) return
+ if (!result.data || result.data.trim() === '') {
+ setState({ kind: 'empty' })
+ return
+ }
+ setState({ kind: 'loaded', html: result.data })
+ } catch (err) {
+ if (cancelled) return
+ const message = err instanceof Error ? err.message : String(err)
+ setState({ kind: 'error', message })
+ }
+ })()
+
+ return () => {
+ cancelled = true
+ }
+ }, [path])
+
+ if (state.kind === 'error') {
+ return (
+
+
+
Could not load preview
+
{state.message}
+
{path}
+
+ )
+ }
+
+ if (state.kind === 'empty') {
+ return (
+
+ )
+ }
+
+ if (state.kind === 'tooLarge') {
+ return (
+
+
+
File too large to preview
+
+ {state.sizeMB.toFixed(1)} MB — preview limit is {(MAX_SIZE_BYTES / (1024 * 1024)).toFixed(0)} MB.
+
+
+
+ )
+ }
return (
- {html && (
+ {state.kind === 'loaded' && (