Repository navigation
[#912 Slice 5] Image/PDF rendering + no-project fallback + state persistence #923
Description
Important
Problem — After Slices 2–4 deliver directory navigation and the CodeMirror editor for markdown, the Preview tab still can't display images or PDFs, has no fallback for sessions without a project, and doesn't flush unsaved edits on navigate-away.
Approach — Complete preview-file-view.tsx (created in Slice 2 with markdown-only routing) with image and PDF rendering branches (view-only, with zoom controls). Add no-project fallback to the orchestrator: when no project is associated, use the existing touchedFiles prop (already passed to the preview tab by session-side-panel.tsx) filtered to renderable extensions. Wire navigate-away debounce flush and verify full state persistence across tab switches.
Scope — in: image rendering, PDF rendering, view-only mode (hide toggle for non-markdown), zoom controls for all types, no-project fallback via existing prop, debounce flush on navigate-away, state persistence verification · out: no further navigation changes, no CodeMirror changes
Acceptance Criteria
-
preview-file-view.tsximage branch: files matching image extensions (.png,.jpg,.jpeg,.gif,.svg,.webp,.ico,.bmp) render as<img>centered in the content area, scaled to fit the panel width -
preview-file-view.tsxPDF branch:.pdffiles render via<embed type="application/pdf">filling the content area - Zoom controls (50%–200%) work for images, PDFs, and markdown (both Preview and Edit modes) — applied via CSS
transform: scale()on the content container, consistent with the existing zoom implementation - The Preview/Edit mode toggle (
SegmentedControlV2) is hidden when viewing an image or PDF (view-only types) - No-project fallback: when the session has no project directory, the tab uses the
touchedFilesprop (already injected bysession-side-panel.tsxfrom theGET /session/:id/touched-filesquery) filtered withisRenderable()frommarkdown-utils.ts, rendered as a flat list - In no-project mode, the search bar and breadcrumb are hidden (only the flat file list and file view are available)
- In no-project mode, clicking a file still opens
preview-file-view.tsxwith the full file view (preview/edit for markdown, view-only for images/PDFs) - Navigating away from a file with a pending autosave flushes the debounce timer (immediate save, not canceled) — this is wired in the orchestrator's
selectedFilechange effect - Full navigation state persists across tab switches: current path, selected file, per-file mode, scroll position, unsaved content
- Returning to a previously viewed file restores its mode (Preview/Edit), scroll position, and any unsaved content
Testing Decisions
Test the file-type routing in preview-file-view.tsx: given a file extension, verify the correct viewer component is rendered and the mode toggle visibility is correct. Test the no-project fallback path: mock the absence of a project directory and verify the flat list renders from touchedFiles prop. Test the debounce flush behavior: navigate away with a pending save and verify the write fires immediately. Extends Slice 2's navigation state tests for persistence verification.
Key Decisions
Images use <img> with CSS scaling. The image is rendered as <img src={fileUrl}> inside a centered container. Zoom is applied via CSS transform: scale() on the container, same as the existing markdown zoom. No canvas rendering or image manipulation library needed.
PDFs use <embed>. <embed type="application/pdf" src={fileUrl}> delegates to Chromium's built-in PDF renderer (which VS Code's webview uses). Zoom is applied via the container's CSS transform.
No-project detection uses existing touchedFiles prop. The SessionPreviewTab component already receives touchedFiles as a prop from session-side-panel.tsx (which queries GET /session/:id/touched-files). When no project directory is associated with the session, the orchestrator skips the useFile() tree API entirely and filters touchedFiles with isRenderable(). This matches the current architecture — no new endpoint fetching needed.
Debounce flush on navigate-away. When selectedFile changes (via createEffect in the orchestrator), any pending debounce timer for the previous file is flushed (fires immediately), not canceled. This prevents data loss without requiring a confirmation dialog.
Constraints & Invariants
- Zoom controls must apply consistently across all file types and all applicable view modes
- The no-project fallback must be visually coherent — no broken breadcrumb or empty search bar (both are hidden)
- State persistence must survive rapid tab switching (no race conditions between save and restore)
- The
touchedFilesprop and theuseFile()tree path both filter with the sameisRenderable()function frommarkdown-utils.ts— never a local extension list
Prior Art
session-preview-tab.tsx— existing zoom controls (thezoomLevelsignal and CSS transform pattern) and per-file state trackingpreview-file-view.tsx(from Slice 2) — the routing component with markdown branch filled in, image/PDF branches as stubspreview-editor.tsx(from Slice 4) — the debounce save mechanism to flush on navigate-awaysession-side-panel.tsx:818— wheretouchedFilesis passed as a prop toSessionPreviewTab
Activity
jeonghun-jj-lee commentedon Sep 9, 2026
Superseded by #931 (Preview as sidebar companion). The Finder-based design is replaced by the reactive companion model — sidebar drives file selection, Preview is a pure viewer.