diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx
index 915e3516..39cf44e2 100644
--- a/apps/x/apps/renderer/src/App.tsx
+++ b/apps/x/apps/renderer/src/App.tsx
@@ -13,11 +13,10 @@ import { ChatInputWithMentions, type StagedAttachment } from './components/chat-
import { ChatMessageAttachments } from '@/components/chat-message-attachments'
import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view';
import { BasesView, type BaseConfig, DEFAULT_BASE_CONFIG } from '@/components/bases-view';
-import { HtmlFileViewer } from '@/components/html-file-viewer';
import { ImageFileViewer } from '@/components/image-file-viewer';
import { VideoFileViewer } from '@/components/video-file-viewer';
-import { PdfFileViewer } from '@/components/pdf-file-viewer';
import { AudioFileViewer } from '@/components/audio-file-viewer';
+import { PersistentViewerCache } from '@/components/persistent-viewer-cache';
import { useDebounce } from './hooks/use-debounce';
import { SidebarContentPanel } from '@/components/sidebar-content';
import { SuggestedTopicsView } from '@/components/suggested-topics-view';
@@ -4722,6 +4721,15 @@ function App() {
/>
) : selectedPath ? (
+ <>
+ {/* Always-mounted persistent cache for HTML/PDF — hidden when active file is something else, so iframes preserve scroll/page/zoom across switches. */}
+
+ {!/\.(html?|pdf)$/i.test(selectedPath) && (
selectedPath.endsWith('.md') ? (
@@ -4831,10 +4839,6 @@ function App() {
/>
)}
- ) : selectedPath?.toLowerCase().endsWith('.html') || selectedPath?.toLowerCase().endsWith('.htm') ? (
-
-
-
) : selectedPath && /\.(png|jpe?g|webp|gif|svg|avif|bmp|ico)$/i.test(selectedPath) ? (
@@ -4843,10 +4847,6 @@ function App() {
- ) : selectedPath?.toLowerCase().endsWith('.pdf') ? (
-
) : selectedPath && /\.(mp3|wav|m4a|ogg|flac|aac)$/i.test(selectedPath) ? (
@@ -4858,6 +4858,8 @@ function App() {
)
+ )}
+ >
) : selectedTask ? (
+ }
+ if (lower.endsWith('.pdf')) {
+ return
+ }
+ return null
+}
+
+interface PersistentViewerCacheProps {
+ activePath: string
+}
+
+/**
+ * Keeps recently-opened HTML and PDF viewers mounted in the DOM,
+ * toggling visibility instead of unmounting. This preserves iframe
+ * state (PDF page/zoom, HTML scroll/JS state) across file switches.
+ */
+export function PersistentViewerCache({ activePath }: PersistentViewerCacheProps) {
+ const [mountedPaths, setMountedPaths] = useState
(() =>
+ isCacheable(activePath) ? [activePath] : []
+ )
+
+ useEffect(() => {
+ if (!isCacheable(activePath)) return
+ setMountedPaths((prev) => {
+ if (prev.includes(activePath)) {
+ // Move to most-recent position
+ return [...prev.filter((p) => p !== activePath), activePath]
+ }
+ const next = [...prev, activePath]
+ return next.length > CACHE_LIMIT ? next.slice(-CACHE_LIMIT) : next
+ })
+ }, [activePath])
+
+ return (
+
+ {mountedPaths.map((p) => (
+
+ {renderViewer(p)}
+
+ ))}
+
+ )
+}