feat: image and PDF preview in the file pane#75
Conversation
|
Working on the changelog and conflicts |
|
No need to add a changelog entry. I will add them all to a new entry because I will group more fixes in a release. Thanks |
simion
left a comment
There was a problem hiding this comment.
Thanks for this, the feature is wanted and the security posture (allowlist, cap, containment, spawn_blocking) is exactly right. Two things before it can merge:
1. CSP: approved. Widening object-src from 'none' to 'self' data: for the PDF <embed> is fine, same status as the img-src data: exception from #65. No change needed there.
2. Please rebase onto current main. The branch predates two things that landed recently, so it conflicts and duplicates infrastructure:
- The workspaces-to-tasks rename:
src/components/workspace/is nowsrc/components/task/,WorkspaceViewisTaskView, and the Rust side usesTask/task_*naming throughout. - The markdown-preview images work already added most of this PR's Rust plumbing, in more evolved form:
read_capped_fileexists on main and is hardened withf.take(cap + 1)plus a post-read length check (your copy's unboundedread_to_endcan exceed the cap if the file grows between the fstat and the read).image_mime_for_extexists with the identical image allowlist.task_file_read_base64exists and has anmtime:lenfingerprint short-circuit (known_fp/unchanged) so an agent settle doesn't re-read and re-encode unchanged multi-MB files.
So the rebased PR gets much smaller:
- Add
"pdf" => Some("application/pdf")toimage_mime_for_ext(and rename it to something likepreview_mime_for_ext). Note the existing testimage_mime_for_ext_rejects_non_imagesexplicitly asserts.pdfis rejected; update it to assert.pdfis accepted instead. - Keep
previewPaths.ts+ tests and theTaskViewrouting as-is (modulo the rename). - Rewire
PreviewPaneto call the existingtaskFileReadBase64(id, path, knownFp)and keep the returnedfpin state so thefsRevisionre-fetch benefits from the unchanged short-circuit instead of re-paying a full read + base64 encode per settle tick. - Drop the new
workspace_file_read_base64command, the duplicateread_capped_file, and the directbase64Cargo dependency (already there). - Keep the CSP change and add a
CHANGELOG.mdentry (Keep a Changelog format, no em dashes, new section at the top).
The end-to-end tests you added (roundtrip, member-path resolution, non-previewable rejection) are worth porting onto task_file_read_base64's test coverage. Ran your branch as-is: vitest 3/3, cargo test 119/119, tsc + vite build clean, so the logic is sound, it just needs to land on today's main.
|
On it |
28458f1 to
f4b5754
Compare
Clicking an image or PDF in the file tree previously opened it as a code-editor tab and failed with a generic "not valid UTF-8" error. Route those extensions to a new read-only PreviewPane (img/embed fed a base64 data URL) instead of CodeMirror, reusing the existing task_file_read_base64 command (added for markdown image preview) instead of introducing a parallel one. preview_mime_for_ext extends image_mime_for_ext with a PDF case, kept as a separate function so image_mime_for_ext's existing image-only contract (and its test asserting .pdf -> None) stays intact. task_file_read_base64 is split into a thin command wrapper plus a task_file_read_base64_for_task helper (mirroring task_file_diff_sides_for_task), so it's testable end-to-end with an in-memory Task instead of load_tasks(): root-file read+decode roundtrip, member-composition path resolution, known_fp short-circuit, and non-previewable-extension rejection. Loosens object-src in the CSP from 'none' to 'self' data: so <embed> can render PDF data URLs. This is a deliberate, scoped exception alongside the existing img-src https: one.
Left unversioned since entries in this repo are normally written by the maintainer during their own release cut (simion#66).
f4b5754 to
d547bd8
Compare
|
Thanks for the detailed review, this is exactly the direction. Both requested changes are in:
Everything else from the rebase (reusing Re-ran everything after these two changes: |
PreviewPane only reset error state on a file switch, not on the fsRevision refetch, so a load that failed once (transient error, too large) then succeeded on an agent-settle tick left the error banner rendered on top of the freshly loaded image/PDF. Clear the error in the success path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019x3kbDKdUAfbCKeWhBnmJw
Changelog entries are authored at release time via the release skill, not per-PR. The image/PDF preview line will land in the next version's entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019x3kbDKdUAfbCKeWhBnmJw
Contributors and agents kept adding [Unreleased] changelog entries and were unclear on who cuts releases (see #75). Make it explicit in CONTRIBUTING.md (a new PR rule + a note atop the Releasing section) and CLAUDE.md (a maintainer-only banner on the Releasing section): don't run make release, don't bump the version, don't touch CHANGELOG.md / changelog.json unless the maintainer explicitly asks. Add AGENTS.md as a symlink to CLAUDE.md so codex-family agents pick up the same rules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019x3kbDKdUAfbCKeWhBnmJw
…follow-up) PR #75 fed <embed type="application/pdf"> a base64 data: URL, which renders blank in WKWebView: its PDF support only engages for a real resource load with an application/pdf MIME, not a data: URL. Images were fine (<img> via img-src); PDFs showed an empty pane. Add a custom `taskpdf:` async URI-scheme handler (register_asynchronous_uri_scheme_protocol) that streams the bytes as application/pdf so WKWebView renders them natively. It reuses a new read_preview_file helper, so the SAME member-aware containment (resolve_task_git_path + safe_task_path, rejects ../symlink escapes), extension allowlist (preview_mime_for_ext, image/PDF only), and 20 MB cap as the base64 channel apply. A rejected path is a 404, not a generic file read. PreviewPane points the embed at taskpdf://localhost/<enc id>/<enc path>?v=<fsRevision>; the v= param busts the webview cache on agent-settle so a rewritten PDF reloads. CSP: object-src goes from 'self' data: to 'self' taskpdf: (data: is dropped since PDFs no longer use it and images render via img-src), a net tightening. taskpdf: is not in img-src/connect-src, so only <embed>/<object> can reach it. percent-encoding is pinned direct (already in the tree via tauri/url; no new crate, no binary-size impact). Tests: preview-read roundtrip + non-previewable rejection, and taskpdf_response 400 (malformed) / 404 (unknown task) paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019x3kbDKdUAfbCKeWhBnmJw
|
Thanks @adamatan, merged and shipped. Genuinely clean PR: the TOCTOU-safe capped read (fstat on the open handle + Two small follow-ups landed on 1. Stale error banner ( 2. PDF rendering ( On the changelog: I pulled the |
Summary
fixes #66
Clicking an image or PDF in the file tree opened it as a code-editor tab and
failed with a generic "not valid UTF-8, so it can't be shown" error. Both now
render inline.
Extension-based routing (
previewKindForPathinsrc/lib/previewPaths.ts)sends image/PDF tabs to a new read-only
PreviewPaneinstead ofEditorPane.The pane fetches bytes over a new
workspace_file_read_base64Tauri command(same member-aware resolution + worktree containment as
workspace_file_read,plus an extension whitelist and a 20 MB cap) and renders an
<img>for imagesor an
<embed type="application/pdf">for PDFs, fed a base64 data URL. NoCodeMirror instance is created, so there's no dirty/unsaved state to worry
about; the pane just re-fetches silently on the workspace's
fsRevisiontick(agent-settle) so a preview an agent just wrote reflects on screen.
Rust side:
read_capped_fileis TOCTOU-safe (fstat on the already-openhandle, not a separate path-based
metadata()call), so a symlink retarget ortruncate-and-replace between the check and the read can't smuggle a larger or
non-regular file past the cap.
preview_mime_for_extis a hardcodedextension allowlist so this stays a preview-only channel, not a generic
binary-read path.
workspace_file_read_base64is split into a thin commandwrapper plus a
_for_workspacehelper (mirroringworkspace_file_diff_sides_for_workspace), so it's testable end-to-end withan in-memory
Workspaceinstead of needingload_workspaces().CSP:
object-srcloosens from'none'to'self' data:so<embed>canrender a
data:application/pdf;...URL.img-srcalready alloweddata:from #65, so images needed no CSP change.
Changelog: added an
## [Unreleased]entry at the top ofCHANGELOG.mdrather than a versioned section, since entries in this repo are normally
written by the maintainer during their own release cut. Feel free to fold it
into whichever version this lands in, or drop it if you'd rather add it
yourself at release time.
Tradeoffs / open questions
(
preview_mime_for_extin Rust,previewKindForPathin TS) — samepattern (and same caveat) as the allowlist duplication called out in feat: images and links in markdown preview #65.
Test plan
npm test—previewPaths.test.ts(image/PDF/non-previewable extensionrouting)
cargo test --lib— mime allowlist, capped-read within/over cap, directoryrejection, plus end-to-end
workspace_file_read_base64coverage(root-file roundtrip, member-composition path resolution,
non-previewable-extension rejection)
npx tsc -b,npm run build.png/.svg/.pdffrom the file tree, confirm it rendersinstead of the binary error; confirm a
.ts/.mdfile still opens in thenormal editor/markdown pane
🤖 Generated with Claude Code