feat(files): preview HEIC photos in the file viewer - #6350
Conversation
The agent can read HEIC since #6346, but the Files page still showed 'Preview not available' — an <img> pointed at the serve route got the stored HEIF under nosniff, which no browser outside Safari renders. The serve route now resolves a JPEG derivative for HEIF bytes, cached in the artifact store and keyed by the source's storage key. Workspace keys are regenerated on every content replacement, so the key is already a content version and using it avoids streaming the original just to hash it. Caching matters here in a way it did not for the vision path: a preview is re-fetched on every view and the WASM decode costs roughly a second for a phone photo. The original stays the stored object — downloads and raw=1 serve it untouched, so this never changes what a user gets back. compileDocumentIfNeeded becomes resolveServableBytes, since it now resolves images as well as generated documents. .tif/.tiff stay download-only: nothing decodes those on either side.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview The workspace
The UI treats Reviewed by Cursor Bugbot for commit 9165266. Configure here. |
Greptile SummaryThe PR adds cached JPEG derivatives for HEVC-based HEIF previews while preserving original bytes for downloads and raw requests.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/uploads/server/image-derivative.ts | Adds storage-keyed loading, generation, and best-effort caching of JPEG derivatives for HEVC-based HEIF images. |
| apps/sim/app/api/files/serve/[...path]/route.ts | Resolves image derivatives only for preview requests while retaining raw and download behavior. |
| apps/sim/app/api/files/public/[token]/content/route.ts | Adds preview-only derivative resolution to public shared-file content responses. |
| apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/image-preview.tsx | Requests preview derivatives and replaces images that fail to load with the unsupported-preview state. |
| apps/sim/lib/uploads/server/heic.ts | Separates HEVC-specific detection from broad HEIF detection and bounds compatible-brand scanning. |
| apps/sim/hooks/use-file-content-source.tsx | Propagates the preview marker through authenticated and public file-content URLs. |
Sequence Diagram
sequenceDiagram
participant UI as ImagePreview
participant Route as File serve route
participant Cache as Derivative cache
participant Decoder as HEIF decoder
UI->>Route: "GET content?preview=1"
Route->>Cache: Load derivative by storage key
alt Cached JPEG exists
Cache-->>Route: JPEG bytes
else Cache miss
Route->>Decoder: Transcode HEIF to JPEG
alt Transcode succeeds
Decoder-->>Route: JPEG bytes
Route->>Cache: Store derivative
else Transcode fails
Decoder-->>Route: No derivative
Route-->>UI: Original bytes
UI->>UI: onError renders unsupported fallback
end
end
Route-->>UI: Renderable response
Reviews (7): Last reviewed commit: "improvement(copilot): only ask for a pre..." | Re-trigger Greptile
…n image Five issues from review, all interlocking around one decision. The derivative is now requested with preview=1 rather than suppressed with raw=1. raw=1 would have corrupted generated-document downloads: every non-markdown workspace download routes through the serve route and relies on resolveServableDocBytes compiling stored source into the real binary. Opt-in separates the three consumers cleanly — previews get the JPEG, downloads get untouched stored bytes, and doc compilation stays unconditional. - Public shares resolve the derivative too, with the same preview/download split; the viewer requests it, the download button does not. - Split the brand predicate. isHeifContainer stays broad for the vision path, where it only runs after sharp has already failed. The serve path runs first, so it uses isHevcHeifContainer — an AVIF was costing a storage round-trip, a WASM load and a misleading warn per request. - A derivative that cannot be produced (past the 20MB ceiling, or a decode failure) now falls back to 'Preview not available' instead of a broken image. UnsupportedPreview moved to preview-shared to avoid a module cycle. - The chat composer chip requests the derivative, so HEIC attachments stop rendering as broken thumbnails.
|
Pushed a83cfce addressing all four threads plus a fifth issue none of them flagged. The fifth one was the most severe and it changed the design. So the derivative is now opt-in via
Also fixed the chat composer chip ( 1302 tests pass, typecheck and Still not verified in a browser — worth loading an actual |
|
@cursor review |
An overwrite preserves the storage key, which is what the parent keys this component on, so only the URL version changes and it never remounts. The previous bytes' outcome therefore stuck, leaving a replaced image parked on 'Preview not available' until something else forced a remount. Reset on URL change during render rather than in an effect — this is derived state, and an effect would render the stale outcome first.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 76e496d. Configure here.
…d scan
- Content writes mint a new storage key, so the parent's key={file.key}
already remounts ImagePreview; the render-phase reset was unreachable and
made renames flash a loading overlay.
- Clamp the ftyp compatible-brand scan to a real box size. The declared size
is attacker-controlled and this now runs on every preview request.
- UnsupportedPreview takes a primitive name so memo is load-bearing.
- Fix the hardcoded ? in the public preview URL builder.
|
@cursor review |
…bnails A video has no derivative path, so preview=1 there only spent a brand sniff per request. Adds the missing test coverage for the helper.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9165266. Configure here.
|
@greptile The 4/5 rests on a premise that is factually incorrect for this codebase: that a content replacement is written under the same storage key. It is not — every byte-changing write mints a brand-new key, so the parent's Evidence:
I audited every This is also why An earlier reply on this PR did claim the fix landed in Separately, this round did fix two real issues: the |
|
@cursor review |
|
You're right, and the evidence is conclusive. The premise behind that concern was wrong. The three pointers in The earlier reply on this PR that claimed otherwise was incorrect on the mechanism, and the revert of The two real fixes you call out — the |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9165266. Configure here.
Summary
The agent can read HEIC since #6346, but the Files page still showed "Preview not available for .heic files". An
<img>pointed at the serve route received the stored HEIF underX-Content-Type-Options: nosniff, which no browser outside Safari renders.The serve route now resolves a JPEG derivative for HEIF bytes and caches it, so
.heicuploads preview like any other image.Design
?raw=1serve it untouched, so this never changes what a user gets back.compileDocumentIfNeededbecomesresolveServableBytes, since it now resolves images as well as generated documents.Scope
.tif/.tiffstay download-only — nothing decodes those on either side, so previewing them would show a broken image rather than a picture. That exclusion is now the only one, and the comment says why.Type of Change
Testing
Six tests on the resolver covering passthrough for non-HEIF, transcode-and-cache on a miss, cache hit without re-decoding, storage-key derivation (replaced content misses the old entry), image still served when caching fails, and null when the decode fails. Viewer categorisation tests updated. 681 tests, typecheck, lint, and
check:api-validationpass.Not verified in a browser — worth a look at an actual
.heicin the Files page before merge.Checklist