You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Preview — PDF rendering (pdfjs-dist, zoom, scroll-position preservation)
Important
Problem — Amicode cannot display PDF files. Researchers writing papers must open a separate PDF viewer to read compiled documents, figures, or reference papers.
Approach — Add pdfjs-dist as an eager-bundled dependency. Build a PDFViewer component that takes a file path, fetches the content as binary (base64 via the file API), renders pages to canvas elements in a scrollable container, and supports zoom controls. The viewer preserves scroll position across document reloads (critical for TeX compilation hot-reload in slice 5). Wired into the Preview panel's renderer dispatch (slice 2) for .pdf files.
Scope — in: pdfjs-dist bundling + Vite worker config, PDFViewer component, canvas-based page rendering, zoom controls, scroll preservation on reload. out: PDF annotation, text selection, search-within-PDF, split-pane source+PDF view.
Acceptance Criteria
pdfjs-dist is added as a dependency with its worker file configured as a separate Vite asset (not inlined)
PDFViewer component accepts a file path, fetches binary content via the SDK file API, and renders all pages to <canvas> elements in a scrollable container
Zoom controls (in/out/reset, range ~50%–200%) are integrated — extract the inline zoom JSX from session-preview-tab.tsx:246-279 (currently a <input> + two <button> elements with local signals, not a reusable component) into a shared ZoomControl component, then reuse it here
Scroll position is preserved when the document reloads (same file path, new content — the TeX recompilation case)
Loading state shown while PDF is being fetched/rendered
Error state shown when the PDF fails to load or is corrupted ("Could not load PDF")
The viewer is always read-only — no editing, no annotation
Pages render at sufficient resolution for the current zoom level (device pixel ratio aware)
The component is self-contained — mountable by the renderer dispatch without knowledge of PDF internals
Testing Decisions
Unit test the PDF viewer's loading/error state transitions (mock pdfjs-dist document loading)
Unit test zoom control range clamping and scroll-position restoration logic
Integration test: render a small fixture PDF (1–2 pages) and verify canvas elements are created
Vite config test: verify the worker file is emitted as a separate asset (build output check)
Key Decisions
Eager bundle.pdfjs-dist is imported statically, not lazy-loaded. Assets are local (embedded in the binary), so eager import eliminates an async boundary. The worker file must be configured as a separate Vite asset — the current vite.config.ts (packages/app/vite.config.ts) has no worker configuration; add build.rollupOptions or use Vite's ?worker&url import suffix to emit pdf.worker.mjs as a separate chunk.
Bundle size.pdfjs-dist adds ~400-500KB (main library) + ~350KB (worker). Combined with the CodeMirror addition in slice 4 (~150KB), the total webview payload grows by ~1MB gzipped. This is acceptable for the functionality gained but should be verified against the Amicode binary size budget after integration.
Canvas rendering, not SVG. Canvas is faster for multi-page documents and handles complex PDF content (embedded fonts, vector graphics) more reliably than SVG rendering.
Scroll preservation via page index + offset. On reload, store the current top-most visible page index and the scroll offset within that page. After the new document renders, restore to the same position. This handles the TeX hot-reload case where page count may change slightly.
No text layer. Text selection within the PDF is out of scope. The canvas renders the visual content only. This simplifies the implementation and avoids the text-layer positioning issues that plague many PDF viewers.
Constraints & Invariants
The PDF viewer is always read-only
The viewer handles malformed/corrupted PDFs gracefully (error state, not crash)
The pdfjs-dist worker is never inlined — always a separate asset
Zoom state is local to the PDF viewer instance (not shared with other renderers)
Prior Art
pdfjs-dist — Mozilla's PDF rendering library (the standard in web PDF rendering)
Zoom controls in session-preview-tab.tsx:246-279 — inline JSX (<input> + two <button> elements with local zoom/zoomIn/zoomOut signals). Must be extracted into a shared ZoomControl component as a prerequisite of this slice.
File API: GET /file/content?path=... returns { type: "binary", content: "<base64>", encoding: "base64" } for PDFs (confirmed in SDK FileContent type at types.gen.ts:2296-2314)
Source
harmoniqs/opencode — new PDFViewer component in packages/app/src/components/, Vite config in packages/app/vite.config.ts
6. Vite config — add to the desktop plugin's config() return at vite.js:26:
optimizeDeps: {include: ['pdfjs-dist'],},
Bundle size
pdfjs-dist adds ~400-500KB (main) + ~350KB (worker) gzipped. The worker is loaded as a separate file (not inlined) thanks to the ?url import pattern. Verify final bundle size after integration with npx vite build --report.
Verification
# Unit tests (opencode app uses bun test, not vitest)cd~/harmoniqs/opencode/packages/app && bun test --conditions=solid --preload ./happydom.ts src/components/pdf-viewer.test.ts
cd~/harmoniqs/opencode/packages/app && bun test --conditions=solid --preload ./happydom.ts src/components/zoom-control.test.ts
# Test expectations:# - PDFViewer renders N canvas elements for N-page PDF (mock pdfjs-dist)# - ZoomControl displays percentage, fires callbacks# - Scroll preservation: save/restore round-trips correctly# - Error state: corrupted PDF shows error message, not crash# Vite build check:cd~/harmoniqs/opencode && npx vite build 2>&1| grep -i worker # verify pdf.worker.mjs emitted# Visual check in Extension Dev Host (F5):# 1. Open a project with a .pdf file# 2. Click the PDF in the Preview tree → pages render in canvas# 3. Zoom in/out → pages re-render at higher/lower resolution# 4. Scroll to page 5, trigger a re-render → scroll stays at page 5
Preview — PDF rendering (pdfjs-dist, zoom, scroll-position preservation)
Important
Problem — Amicode cannot display PDF files. Researchers writing papers must open a separate PDF viewer to read compiled documents, figures, or reference papers.
Approach — Add
pdfjs-distas an eager-bundled dependency. Build aPDFViewercomponent that takes a file path, fetches the content as binary (base64 via the file API), renders pages to canvas elements in a scrollable container, and supports zoom controls. The viewer preserves scroll position across document reloads (critical for TeX compilation hot-reload in slice 5). Wired into the Preview panel's renderer dispatch (slice 2) for.pdffiles.Scope — in:
pdfjs-distbundling + Vite worker config,PDFViewercomponent, canvas-based page rendering, zoom controls, scroll preservation on reload. out: PDF annotation, text selection, search-within-PDF, split-pane source+PDF view.Acceptance Criteria
pdfjs-distis added as a dependency with its worker file configured as a separate Vite asset (not inlined)PDFViewercomponent accepts a file path, fetches binary content via the SDK file API, and renders all pages to<canvas>elements in a scrollable containersession-preview-tab.tsx:246-279(currently a<input>+ two<button>elements with local signals, not a reusable component) into a sharedZoomControlcomponent, then reuse it hereTesting Decisions
pdfjs-distdocument loading)Key Decisions
Eager bundle.
pdfjs-distis imported statically, not lazy-loaded. Assets are local (embedded in the binary), so eager import eliminates an async boundary. The worker file must be configured as a separate Vite asset — the currentvite.config.ts(packages/app/vite.config.ts) has no worker configuration; addbuild.rollupOptionsor use Vite's?worker&urlimport suffix to emitpdf.worker.mjsas a separate chunk.Bundle size.
pdfjs-distadds ~400-500KB (main library) + ~350KB (worker). Combined with the CodeMirror addition in slice 4 (~150KB), the total webview payload grows by ~1MB gzipped. This is acceptable for the functionality gained but should be verified against the Amicode binary size budget after integration.Canvas rendering, not SVG. Canvas is faster for multi-page documents and handles complex PDF content (embedded fonts, vector graphics) more reliably than SVG rendering.
Scroll preservation via page index + offset. On reload, store the current top-most visible page index and the scroll offset within that page. After the new document renders, restore to the same position. This handles the TeX hot-reload case where page count may change slightly.
No text layer. Text selection within the PDF is out of scope. The canvas renders the visual content only. This simplifies the implementation and avoids the text-layer positioning issues that plague many PDF viewers.
Constraints & Invariants
pdfjs-distworker is never inlined — always a separate assetPrior Art
pdfjs-dist— Mozilla's PDF rendering library (the standard in web PDF rendering)session-preview-tab.tsx:246-279— inline JSX (<input>+ two<button>elements with localzoom/zoomIn/zoomOutsignals). Must be extracted into a sharedZoomControlcomponent as a prerequisite of this slice.GET /file/content?path=...returns{ type: "binary", content: "<base64>", encoding: "base64" }for PDFs (confirmed in SDKFileContenttype attypes.gen.ts:2296-2314)Source
harmoniqs/opencode— newPDFViewercomponent inpackages/app/src/components/, Vite config inpackages/app/vite.config.tsImplementation Plan
Files to create
packages/app/src/components/pdf-viewer.tsxPDFViewercomponent: pdfjs-dist canvas rendering + zoom + scroll preservationpackages/app/src/components/zoom-control.tsxsession-preview-tab.tsx:246-279)Files to modify
packages/app/package.jsonpdfjs-distdependencypackages/app/vite.jsoptimizeDeps.include: ['pdfjs-dist']to the desktop plugin config (worker config already exists at line 31-33:worker: { format: "es" })packages/app/src/components/session/session-preview-tab.tsx<ZoomControl>importpackages/app/src/components/session/preview-content-area.tsx<PDFViewer>Integration points (exact)
1. pdfjs-dist worker setup — use Vite's
?urlimport suffix (the desktop plugin atvite.js:31already setsworker: { format: "es" }):2. Binary content decoding — the SDK returns base64 in
FileContent.contentwhentype === "binary". Decode toUint8Arrayfor pdfjs-dist:3. PDFViewer component — mount via ref + onMount + onCleanup (matching the pierre/diffs pattern at
session-ui/src/components/file-ssr.tsx:90-145):4. Scroll preservation — track page index + offset:
5. Extract ZoomControl — currently inline JSX at
session-preview-tab.tsx:246-279:6. Vite config — add to the desktop plugin's
config()return atvite.js:26:Bundle size
pdfjs-distadds ~400-500KB (main) + ~350KB (worker) gzipped. The worker is loaded as a separate file (not inlined) thanks to the?urlimport pattern. Verify final bundle size after integration withnpx vite build --report.Verification