Skip to content

feat: add attachment previews and file type icons#153

Open
IntoTheBlueAt wants to merge 4 commits into
erictli:mainfrom
IntoTheBlueAt:main
Open

feat: add attachment previews and file type icons#153
IntoTheBlueAt wants to merge 4 commits into
erictli:mainfrom
IntoTheBlueAt:main

Conversation

@IntoTheBlueAt
Copy link
Copy Markdown

@IntoTheBlueAt IntoTheBlueAt commented May 5, 2026

Summary

  • Display non-markdown files (images, PDFs, text, and generic files) in the sidebar folder tree and note list with file type icons
  • Preview panel: inline images, embedded PDF viewer with fallback, syntax-highlighted text files, and "no preview" placeholder for unknown types
  • Toolbar actions: copy path, reveal in file manager, reload preview
  • Security: path canonicalization prevents directory traversal on all file read operations
  • 10 MB size limit on text file preview to prevent UI freezes
  • Image load error handling with fallback UI
  • Use FileIcon (not ImageIcon) for unknown file type fallback

Closes #151

Changes

  • Backend (lib.rs): new Tauri commands for reading text attachments, opening file previews, importing files; attachment scanning integrated into existing folder/note listing
  • Frontend: FilePreview, FileTypeIcon components; updates to FolderTreeView, NoteList, NotesContext, App
  • Types: AttachmentMetadata, AttachmentKind added to note.ts

Test plan

  • Add image files to notes folder → appear in sidebar with image icon, preview shows inline
  • Add PDF → icon in sidebar, embedded viewer or fallback + reveal
  • Add .txt/.json/.csv → text preview with size limit enforced
  • Add .docx or unknown extension → generic file icon, "no preview" with reveal button
  • Delete a previewed image → error fallback shown, reload works
  • Verify path traversal blocked (file outside notes folder)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Attachments support: add/manage non-markdown files (images, PDFs, text, generic files)
    • File preview integrated into main view with reload, copy filepath, and “Reveal in File Manager”
    • List and open text attachments (safe text loading); attachments discoverable via file watcher
  • UI

    • Attachments shown in folder tree and note list (including root-level) with keyboard navigation and context menus
    • Sidebar count includes attachments; new file-type icons and preview header (size/modified)

IntoTheBlueAt and others added 2 commits May 5, 2026 14:17
Add image onError fallback UI, 10MB size limit for text file preview,
and wire up AttachmentKind::File for unknown extensions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0cbc8893-1d4b-46b1-baa0-0968aab8248f

📥 Commits

Reviewing files that changed from the base of the PR and between 2c467f9 and 7f2b5e1.

📒 Files selected for processing (1)
  • src/components/preview/FilePreview.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/preview/FilePreview.tsx

📝 Walkthrough

Walkthrough

Adds attachment support across backend and frontend: Rust Tauri commands to list/read attachments and updated watcher events; frontend types, services, context, folder-tree, lists, icons, and a FilePreview component to list, select, and preview images, PDFs, text, and generic files.

Changes

Attachment Support Integration

Layer / File(s) Summary
Backend Types & Helpers
src-tauri/src/lib.rs
Introduce AttachmentKind and AttachmentMetadata; helpers: attachment_kind_from_extension, attachment_from_abs_path.
Backend Commands
src-tauri/src/lib.rs
Add list_attachments (walk notes folder, return sorted metadata) and read_text_attachment (canonicalize, containment check, text-extension allowlist, 10MB cap).
Watcher & Events
src-tauri/src/lib.rs
Watcher skips excluded/ignored dirs, treats supported non-markdown files as attachments when no note id, avoids Tantivy indexing for non-note files, extends FileChangeEvent with changed_paths, emits changed_ids/changed_paths.
Tauri Config
src-tauri/tauri.conf.json
Extend CSP to add frame-src and object-src allowing self and asset origins for embedded previews.
Frontend Types & Services
src/types/note.ts, src/services/notes.ts, src/services/files.ts
Add AttachmentKind/AttachmentMetadata, extend FolderNode.attachments; export listAttachments() and readTextFile() to call backend.
Context / State
src/context/NotesContext.tsx
Add attachments, selectedAttachment, selectAttachment; refreshNotes() loads attachments; file-change handling checks changed_paths for attachment updates; selection flows clear counterpart selection.
Folder Tree Data & Traversal
src/lib/folderTree.ts
buildFolderTree(attachments) populates folder.attachments and rootAttachments; TreeItem adds attachment/folder; getVisibleItems() and countNotesInFolder() include attachments.
UI Icons & List Item API
src/components/icons/index.tsx, src/components/ui/index.tsx, src/components/notes/FileTypeIcon.tsx
Add FileIcon, FileTextIcon, FilePdfIcon; ListItem gains optional icon prop; FileTypeIcon maps kinds to icons.
Folder Tree & Note List UI
src/components/notes/FolderTreeView.tsx, src/components/notes/NoteList.tsx
Render attachment rows (root and within folders) with selection, context menu actions (copy filepath, reveal in file manager), keyboard navigation, and search filtering.
Main Content Preview & App Routing
src/components/preview/FilePreview.tsx, src/App.tsx
Add FilePreview for image/pdf/text/file kinds (cache-busting, async text loading with stale-response guard, error fallbacks); App conditionally renders preview when selectedAttachment is set.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant Frontend as Frontend (React)
    participant Context as NotesContext
    participant Services as Services (notes/files)
    participant Backend as Tauri (Rust)
    participant FS as Filesystem

    User->>Frontend: click attachment
    Frontend->>Context: selectAttachment(attachment)
    Context->>Frontend: selectedAttachment updated
    Frontend->>Services: readTextFile(path) [if text]
    Services->>Backend: invoke("read_text_attachment", path)
    Backend->>FS: canonicalize & validate path, check ext & size, read file
    FS-->>Backend: file contents / error
    Backend-->>Services: return contents or error
    Services-->>Frontend: deliver text or error
    Frontend-->>User: render FilePreview (image/pdf/text/file)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • erictli/scratch#102: Adds/changes folder-tree structures and traversal that this PR augments to include attachments.
  • erictli/scratch#1: Modifies src-tauri/src/lib.rs around Tauri command handlers and watcher event payloads overlapping with this PR.
  • erictli/scratch#2: Also updates the Tauri backend (commands/registration) related to this PR's additions.

Suggested reviewers

  • erictli

🐰 I found a file and gave it a peek,
Images, PDFs, and text I seek.
Safe reads, previews, folders wide,
Click to view—I'll hop inside! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes by highlighting the two primary features: attachment previews and file type icons for non-markdown files.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #151 by implementing attachment support with in-app previews for PDFs, images, and text files, making attachments visible in the sidebar, and enabling users to view files without leaving the app.
Out of Scope Changes check ✅ Passed All changes directly support the attachment preview feature scope. Backend adds attachment listing and safe reading; frontend adds preview/icon components and integrates attachments into UI.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/services/files.ts (1)

21-23: 💤 Low value

Consider naming the helper to match its actual contract.

readTextFile reads as a generic "any text file" helper, but it invokes read_text_attachment, which only succeeds for paths inside the notes folder with a specific extension allowlist. Renaming to readTextAttachment would prevent future callers from misusing it for non-attachment paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/files.ts` around lines 21 - 23, The helper readTextFile is
misleading because it always calls invoke("read_text_attachment") which only
works for attachment paths in the notes folder; rename the function to
readTextAttachment to match its contract (change function name readTextFile to
readTextAttachment and update all callers/imports accordingly) and keep the
invoke("read_text_attachment", { path }) call unchanged so callers cannot
mistakenly use it for arbitrary text files.
src-tauri/src/lib.rs (2)

2029-2072: 💤 Low value

read_text_attachment LGTM.

Canonicalization on both folder_root and file_path plus a starts_with containment check is the right way to defeat traversal/symlink escapes. The extension allowlist matches the Text arm of attachment_kind_from_extension, and the 10 MB cap is a sensible guardrail before reading into memory.

One small suggestion: since the allowlist must stay in sync with the Text arm in attachment_kind_from_extension, consider extracting it into a shared const TEXT_EXTENSIONS: &[&str] to avoid drift over time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 2029 - 2072, Extract the text-file
extension allowlist into a shared constant (e.g. const TEXT_EXTENSIONS: &[&str])
and replace the hard-coded list in read_text_attachment (function
read_text_attachment) with a lookup against that constant; then update
attachment_kind_from_extension to use the same TEXT_EXTENSIONS constant for its
Text arm so both places share the single source of truth, ensuring the constant
is exported/visible to both modules if needed and preserving the current
case-insensitive comparison (to_ascii_lowercase or compare case-insensitively).

695-704: ⚡ Quick win

attachment_kind_from_extension always returns Some, making Option misleading and overly broad in the watcher.

The _ => Some(AttachmentKind::File) arm means this function returns None only when the extension can't be matched against the lowercase string conversion (which never fails). As a result:

  • The ? on line 726 in attachment_from_abs_path is effectively unreachable.
  • In the file watcher (line 2338-2343), is_attachment becomes true for any file in the notes folder with any extension (e.g. .DS_Store, .swp, editor lock files, partial download tempfiles), spamming file-change events with changed_paths and triggering frontend listAttachments() refetches. The 500 ms debounce reduces but doesn't eliminate this.

Either change the return type to AttachmentKind (and gate "noise" extensions elsewhere), or restrict the catch-all to a known list of "generic" extensions worth surfacing as attachments.

♻️ Suggested approach
-fn attachment_kind_from_extension(ext: &str) -> Option<AttachmentKind> {
-    match ext.to_ascii_lowercase().as_str() {
-        "png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp" | "svg" => Some(AttachmentKind::Image),
-        "pdf" => Some(AttachmentKind::Pdf),
-        "txt" | "text" | "log" | "csv" | "tsv" | "json" | "xml" | "yaml" | "yml" => {
-            Some(AttachmentKind::Text)
-        }
-        _ => Some(AttachmentKind::File),
-    }
-}
+fn attachment_kind_from_extension(ext: &str) -> AttachmentKind {
+    match ext.to_ascii_lowercase().as_str() {
+        "png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp" | "svg" => AttachmentKind::Image,
+        "pdf" => AttachmentKind::Pdf,
+        "txt" | "text" | "log" | "csv" | "tsv" | "json" | "xml" | "yaml" | "yml" => {
+            AttachmentKind::Text
+        }
+        _ => AttachmentKind::File,
+    }
+}

Then in the watcher, gate by an explicit "should treat as attachment" rule (e.g. ignore dotfiles or a small set of known-noise extensions) rather than relying on Option.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 695 - 704, attachment_kind_from_extension
currently always returns Some(AttachmentKind) because of the catch-all arm,
which makes Option misleading and causes attachment_from_abs_path's use of ? to
be unreachable and the watcher to treat every file as an attachment; change
attachment_kind_from_extension to return None for unknown/noise extensions (or
change its signature to return AttachmentKind if you intend every extension to
be treated) and explicitly exclude noise patterns (dotfiles like .DS_Store,
swap/temp files like *.swp, partial download suffixes) before mapping to
AttachmentKind; update the watcher logic that sets is_attachment (and
attachment_from_abs_path usage) to rely on the Option returned (treat None as
not an attachment) or to call a new should_treat_as_attachment predicate that
filters dotfiles and a small whitelist of generic extensions.
src/components/preview/FilePreview.tsx (1)

80-87: ⚡ Quick win

Use navigator.clipboard.writeText instead of the copy_to_clipboard Tauri command.

attachment.path is already available in the frontend, so a backend round-trip via invoke("copy_to_clipboard", …) is unnecessary here.

♻️ Suggested change
   const handleCopyPath = useCallback(async () => {
     try {
-      await invoke("copy_to_clipboard", { text: attachment.path });
+      await navigator.clipboard.writeText(attachment.path);
       toast.success("Copied filepath");
     } catch {
       toast.error("Failed to copy filepath");
     }
   }, [attachment.path]);

If you remove the only remaining invoke() call, the import on line 2 can be reduced to convertFileSrc only.

Based on learnings from PR #103, prefer navigator.clipboard.writeText() in frontend components when the data to copy is already available on the frontend; reserve the copy_to_clipboard Tauri command for cases where the browser Clipboard API is unavailable or when copying data that originates from Rust-side operations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/preview/FilePreview.tsx` around lines 80 - 87, The
handleCopyPath function uses the Tauri invoke("copy_to_clipboard", ...) for data
already available in the frontend; replace the invoke call with
navigator.clipboard.writeText(attachment.path) inside the try block and keep the
same toast.success/toast.error behavior, update the dependency array to still
include attachment.path, and remove the now-unused import of invoke (leaving
convertFileSrc import only) to clean up imports in FilePreview.tsx.
src/context/NotesContext.tsx (1)

670-675: ⚡ Quick win

Widen the listen payload generic instead of casting.

The payload is now used for both notes and attachments, so add changed_paths to the generic to remove the inline as cast and keep the field statically typed.

♻️ Proposed change
-    listen<{ changed_ids: string[] }>("file-change", (event) => {
+    listen<{ changed_ids: string[]; changed_paths?: string[] }>("file-change", (event) => {
       // Don't process if effect was cleaned up
       if (isCancelled) return;

       const changedIds = event.payload.changed_ids || [];
-      const changedPaths = (event.payload as { changed_paths?: string[] }).changed_paths || [];
+      const changedPaths = event.payload.changed_paths || [];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/NotesContext.tsx` around lines 670 - 675, The listen callback
currently types the payload as { changed_ids: string[] } and then casts
event.payload to access changed_paths; update the generic on listen to include
changed_paths (e.g. listen<{ changed_ids: string[]; changed_paths?: string[] }>)
so event.payload.changed_paths is statically typed and remove the inline cast;
keep existing fallback/default behavior for empty arrays (changedIds and
changedPaths) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/preview/FilePreview.tsx`:
- Around line 188-198: In FilePreview.tsx change the icon used in the
unknown-file-type fallback so it reflects non-image files: in the JSX branch
that checks attachment.kind === "file" (the block that renders the "No preview
is available for this file type." text and the Reveal File Button, and uses
handleReveal), replace the ImageIcon with FileIcon (which is already imported
elsewhere in this PR) so the visual matches the file-type fallback.

---

Nitpick comments:
In `@src-tauri/src/lib.rs`:
- Around line 2029-2072: Extract the text-file extension allowlist into a shared
constant (e.g. const TEXT_EXTENSIONS: &[&str]) and replace the hard-coded list
in read_text_attachment (function read_text_attachment) with a lookup against
that constant; then update attachment_kind_from_extension to use the same
TEXT_EXTENSIONS constant for its Text arm so both places share the single source
of truth, ensuring the constant is exported/visible to both modules if needed
and preserving the current case-insensitive comparison (to_ascii_lowercase or
compare case-insensitively).
- Around line 695-704: attachment_kind_from_extension currently always returns
Some(AttachmentKind) because of the catch-all arm, which makes Option misleading
and causes attachment_from_abs_path's use of ? to be unreachable and the watcher
to treat every file as an attachment; change attachment_kind_from_extension to
return None for unknown/noise extensions (or change its signature to return
AttachmentKind if you intend every extension to be treated) and explicitly
exclude noise patterns (dotfiles like .DS_Store, swap/temp files like *.swp,
partial download suffixes) before mapping to AttachmentKind; update the watcher
logic that sets is_attachment (and attachment_from_abs_path usage) to rely on
the Option returned (treat None as not an attachment) or to call a new
should_treat_as_attachment predicate that filters dotfiles and a small whitelist
of generic extensions.

In `@src/components/preview/FilePreview.tsx`:
- Around line 80-87: The handleCopyPath function uses the Tauri
invoke("copy_to_clipboard", ...) for data already available in the frontend;
replace the invoke call with navigator.clipboard.writeText(attachment.path)
inside the try block and keep the same toast.success/toast.error behavior,
update the dependency array to still include attachment.path, and remove the
now-unused import of invoke (leaving convertFileSrc import only) to clean up
imports in FilePreview.tsx.

In `@src/context/NotesContext.tsx`:
- Around line 670-675: The listen callback currently types the payload as {
changed_ids: string[] } and then casts event.payload to access changed_paths;
update the generic on listen to include changed_paths (e.g. listen<{
changed_ids: string[]; changed_paths?: string[] }>) so
event.payload.changed_paths is statically typed and remove the inline cast; keep
existing fallback/default behavior for empty arrays (changedIds and
changedPaths) unchanged.

In `@src/services/files.ts`:
- Around line 21-23: The helper readTextFile is misleading because it always
calls invoke("read_text_attachment") which only works for attachment paths in
the notes folder; rename the function to readTextAttachment to match its
contract (change function name readTextFile to readTextAttachment and update all
callers/imports accordingly) and keep the invoke("read_text_attachment", { path
}) call unchanged so callers cannot mistakenly use it for arbitrary text files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aeec2bf2-f51d-40f2-af08-a1eaf5a8e4b6

📥 Commits

Reviewing files that changed from the base of the PR and between a638f02 and 38734a6.

📒 Files selected for processing (15)
  • src-tauri/src/lib.rs
  • src-tauri/tauri.conf.json
  • src/App.tsx
  • src/components/icons/index.tsx
  • src/components/layout/Sidebar.tsx
  • src/components/notes/FileTypeIcon.tsx
  • src/components/notes/FolderTreeView.tsx
  • src/components/notes/NoteList.tsx
  • src/components/preview/FilePreview.tsx
  • src/components/ui/index.tsx
  • src/context/NotesContext.tsx
  • src/lib/folderTree.ts
  • src/services/files.ts
  • src/services/notes.ts
  • src/types/note.ts

Comment thread src/components/preview/FilePreview.tsx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/preview/FilePreview.tsx`:
- Around line 61-63: Replace the toast that interpolates the raw error with a
generic, user-safe message while keeping the detailed error in console.error; in
FilePreview.tsx update the error handling block that calls console.error("Failed
to read text file:", error), toast.error(`Failed to read file: ${error}`), and
setTextContent("") so that console.error still logs the full error but
toast.error shows a stable message like "Failed to read file. Please try again."
and then call setTextContent("") as before.
- Around line 55-73: loadText can complete out-of-order and overwrite the active
preview; guard against stale results by tagging each load with a generation
token (e.g., a ref counter) or AbortController and only apply results when the
token matches the latest. Concretely: add a requestIdRef and increment it before
calling loadText (or inside useEffect), capture the current id inside loadText,
and before calling setTextContent / setImageError / setIsLoadingText ensure the
captured id === requestIdRef.current (or check !signal.aborted if using
AbortController) so only the most recent readTextFile result updates state;
update references to loadText, setTextContent, useEffect, and
attachment.modified accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 20093149-db59-46fd-b509-8e319156c9f7

📥 Commits

Reviewing files that changed from the base of the PR and between 38734a6 and 2c467f9.

📒 Files selected for processing (1)
  • src/components/preview/FilePreview.tsx

Comment thread src/components/preview/FilePreview.tsx
Comment thread src/components/preview/FilePreview.tsx
… error toast

Prevent out-of-order async loads from overwriting the active preview by
tagging each load with a generation counter. Also replace the raw error
interpolation in the toast with a generic user-safe message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] preview PDFs and Image

1 participant