Skip to content

[#912 Slice 5] Image/PDF rendering + no-project fallback + state persistence #923

Closed

Description

@jeonghun-jj-lee

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.tsx image 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.tsx PDF branch: .pdf files 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 touchedFiles prop (already injected by session-side-panel.tsx from the GET /session/:id/touched-files query) filtered with isRenderable() from markdown-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.tsx with 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 selectedFile change 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 touchedFiles prop and the useFile() tree path both filter with the same isRenderable() function from markdown-utils.ts — never a local extension list

Prior Art

  • session-preview-tab.tsx — existing zoom controls (the zoomLevel signal and CSS transform pattern) and per-file state tracking
  • preview-file-view.tsx (from Slice 2) — the routing component with markdown branch filled in, image/PDF branches as stubs
  • preview-editor.tsx (from Slice 4) — the debounce save mechanism to flush on navigate-away
  • session-side-panel.tsx:818 — where touchedFiles is passed as a prop to SessionPreviewTab

Source

Part of #912 · Blocked by #920 · Blocked by #922

Activity

  1. jeonghun-jj-lee commented on Sep 9, 2026

    @jeonghun-jj-lee
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

afkImplementable without human interaction

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions