Skip to content

feat(clipboard): support image attachments in note content#1162

Open
chhoumann wants to merge 5 commits intomasterfrom
feat/1161-clipboard-image-support
Open

feat(clipboard): support image attachments in note content#1162
chhoumann wants to merge 5 commits intomasterfrom
feat/1161-clipboard-image-support

Conversation

@chhoumann
Copy link
Copy Markdown
Owner

@chhoumann chhoumann commented Apr 7, 2026

Summary

Add clipboard image support to note-content {{clipboard}} rendering.

This keeps {{clipboard}} text-based by default, but when QuickAdd is formatting note content with a destination note context it now detects clipboard images, saves them as Obsidian attachments, and inserts embeds. The resolver also handles desktop clipboard payloads that arrive as local image-file paths, which was necessary to support the live Obsidian clipboard behavior reproduced from issue #1161.

Changes addressing review comments

Alternative approach for local image file reading (src/utilityObsidian.ts):

  • Replaced window.require("fs") with fetch("file://...") for reading local clipboard image paths
  • Removed uint8ArrayToArrayBuffer helper (no longer needed — fetch returns ArrayBuffer directly)
  • Added absolutePathToFileUrl helper for cross-platform path→URL conversion
  • This eliminates the Node.js fs dependency, verbose type casting, and fragile window.require detection

Simplified clipboard handling (src/formatters/completeFormatter.ts):

  • Replaced clipboardContentScopeDepth counter with a simple formattingFileContent boolean flag
  • Removed withClipboardContentScope wrapper method
  • formatFileContent now directly sets/unsets the flag in a try/finally block
  • Simplified resolvedClipboardContent type (removed null union on destinationSourcePath)
  • Net reduction of ~58 lines across implementation and tests

Tests (src/formatters/completeFormatter.clipboard.test.ts):

  • Replaced setWindowRequire helper with mockFetchForFile that mocks globalThis.fetch
  • Updated all local-path tests to assert fetch calls with file:// URLs
  • All 1029 tests pass, lint and build clean

Fixes #1161

Review & Testing Checklist for Human

  • Verify fetch("file://...") works in Obsidian's Electron runtime — test with a clipboard manager (e.g. Clop) that puts local image paths in the clipboard. If fetch is blocked by Electron security policy, the function gracefully returns null and falls back to plain text, but the image import feature would be degraded.
  • Confirm clipboard image embed works end-to-end: copy an image, run a QuickAdd capture/template with {{clipboard}}, verify the attachment is created and embed inserted.
  • Verify formatFileName with {{clipboard}} still returns plain text (not image embeds) even when clipboard contains an image.

Notes

  • The Cloudflare Pages CI failure is pre-existing and unrelated to these changes.
  • The formattingFileContent boolean replaces the depth counter. Nested formatFileContent calls are not expected in practice, so a boolean suffices. If nesting is ever needed, it can be upgraded back to a counter.

Link to Devin session: https://app.devin.ai/sessions/d2e18cb56396423ca07600dfd2facff9
Requested by: @chhoumann

Summary by CodeRabbit

  • New Features

    • Enhanced clipboard handling with support for embedding images directly from clipboard when creating files from templates.
    • Improved clipboard resolution for template operations, including support for resolving local filesystem image paths.
    • Destination-aware clipboard content resolution for better template formatting.
  • Tests

    • Added comprehensive test coverage for clipboard image handling in template creation and file operations.

@chhoumann chhoumann linked an issue Apr 7, 2026 that may be closed by this pull request
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
quickadd Ready Ready Preview Apr 11, 2026 8:37pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4af077c3-7aa9-4815-ba96-1d6aefea32a9

📥 Commits

Reviewing files that changed from the base of the PR and between 05b4a26 and 1393634.

📒 Files selected for processing (1)
  • src/utilityObsidian.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utilityObsidian.ts

📝 Walkthrough

Walkthrough

Adds destination-aware clipboard resolution including image embedding and local-file import for {{clipboard}}; CompleteFormatter gains destination-context APIs and caching; TemplateEngine sets destination context before formatting; utilityObsidian exports resolveClipboardForNoteContent; new clipboard-focused tests and multiple test mock updates added.

Changes

Cohort / File(s) Summary
Clipboard Utility
src/utilityObsidian.ts, src/utilityObsidian.__test
Add clipboard image/text detection, MIME→ext mapping, read/parse clipboard image or local-file paths, import/save attachment bytes to vault, and export resolveClipboardForNoteContent(app, destinationPath?). Expose helpers in __test.
CompleteFormatter core
src/formatters/completeFormatter.ts, src/formatters/formatter.ts
Introduce destination context state and APIs (setDestinationFile, setDestinationSourcePath, clearDestinationContext), per-destination cached resolved clipboard content, formatting-scope flag, change link-source resolution to use destination context, and add fast-path for replaceClipboardInString.
CaptureChoiceFormatter forwarding
src/formatters/captureChoiceFormatter.ts
Forward destination file/path setters to superclass so base formatter receives destination context updates.
Template engine integration
src/engine/TemplateEngine.ts
Set/clear formatter destination context (path or file) immediately before template formatting in create/overwrite/append flows via new private helpers.
Clipboard tests
src/formatters/completeFormatter.clipboard.test.ts
New comprehensive test suite validating clipboard text/image handling, attachment creation/import, caching, and filename handling for formatFileContent and text-only behavior for formatFileName.
Test mocks & engine tests updates
src/engine/..., src/formatters/... (multiple test files)
Extend ../utilityObsidian mocks to include resolveClipboardForNoteContent; expand CompleteFormatter test doubles with setDestinationFile, setDestinationSourcePath, clearDestinationContext, and accessor helpers; update many engine/formatter tests to match new mock surface.

Sequence Diagram(s)

sequenceDiagram
    participant TemplateEngine as TemplateEngine
    participant Formatter as CompleteFormatter
    participant Utility as resolveClipboardForNoteContent
    participant App as App/Clipboard
    participant FileManager as FileManager
    participant Vault as Vault

    TemplateEngine->>Formatter: setDestinationFile(file) / setDestinationSourcePath(path)
    Formatter->>Formatter: store destination context\nenter formattingFileContent scope
    Formatter->>Utility: resolveClipboardForNoteContent(app, destinationPath)
    Utility->>App: navigator.clipboard.read() / readText()
    alt Clipboard image or local-file path found
        App-->>Utility: image Blob or fetched bytes
        Utility->>FileManager: getAvailablePathForAttachment(suggestedName, destinationPath)
        FileManager-->>Utility: availableVaultPath
        Utility->>Vault: createBinary(vaultPath, bytes)
        Vault-->>Utility: created TFile
        Utility->>FileManager: generateMarkdownLink(attachment, destinationPath)
        FileManager-->>Formatter: embed string (![[...]])
    else No image / fallback text
        App-->>Utility: clipboard text (or error -> "")
        Utility-->>Formatter: plain text
    end
    Formatter->>Formatter: replace {{clipboard}} with embed or text
    Formatter-->>TemplateEngine: formatted content
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

released

Poem

🐇 I nibbled bytes from clipboard dew,
Pasted pixels hopped into view,
Vault doors opened, embeds leapt in,
Templates smiled as images grinned,
Hooray — a rabbit’s paste-and-win! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% 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 title 'feat(clipboard): support image attachments in note content' accurately summarizes the main change: adding image attachment support to the clipboard formatter.
Linked Issues check ✅ Passed The PR comprehensively addresses all coding objectives from #1161: clipboard image support with attachment saving, local file path handling, destination-path awareness, text-only preservation, and caching to prevent duplicates.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue #1161. Updates to formatter destination context and clipboard resolution are necessary for the feature, and test mocks are appropriately updated to support the new functionality.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1161-clipboard-image-support

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.

@chhoumann chhoumann marked this pull request as ready for review April 7, 2026 16:26
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 7, 2026

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1393634
Status:🚫  Build failed.

View logs

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

coderabbitai[bot]

This comment was marked as resolved.

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.

🧹 Nitpick comments (1)
src/formatters/completeFormatter.clipboard.test.ts (1)

233-267: Minor inconsistency: missing existsSync assertion.

The PNG test at line 217-219 verifies existsSync was called with the correct path, but this JPEG test omits that assertion. Consider adding it for consistency:

     expect(result).toBe("![[Assets/pasted-image.png]]");
+    expect(app.vault.createBinary).toHaveBeenCalledOnce();
+    // Optionally verify existsSync was called:
+    // expect(existsSync).toHaveBeenCalledWith("/Users/christian/Library/Caches/Clop/images/85074.jpeg");
     expect(app.fileManager.getAvailablePathForAttachment).toHaveBeenCalledWith(

Not blocking since the test still validates the core behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/formatters/completeFormatter.clipboard.test.ts` around lines 233 - 267,
Add the missing assertion that verifies fs.existsSync was called with the JPEG
path in this test; locate the mock fs returned by setWindowRequire and assert
that the existsSync mock (the function returned for id "fs") was invoked with
"/Users/christian/Library/Caches/Clop/images/85074.jpeg" after calling
CompleteFormatter.formatFileContent (the same pattern used in the PNG test), so
the test consistently checks existence checks alongside readFile and
getAvailablePathForAttachment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/formatters/completeFormatter.clipboard.test.ts`:
- Around line 233-267: Add the missing assertion that verifies fs.existsSync was
called with the JPEG path in this test; locate the mock fs returned by
setWindowRequire and assert that the existsSync mock (the function returned for
id "fs") was invoked with
"/Users/christian/Library/Caches/Clop/images/85074.jpeg" after calling
CompleteFormatter.formatFileContent (the same pattern used in the PNG test), so
the test consistently checks existence checks alongside readFile and
getAvailablePathForAttachment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19cf611f-9fe3-4a7f-97f5-c2f9ca8cd3fb

📥 Commits

Reviewing files that changed from the base of the PR and between 3a0d6eb and cdca3f3.

📒 Files selected for processing (3)
  • src/formatters/completeFormatter.clipboard.test.ts
  • src/formatters/formatter.ts
  • src/utilityObsidian.ts
✅ Files skipped from review due to trivial changes (1)
  • src/formatters/formatter.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utilityObsidian.ts

Copy link
Copy Markdown
Owner Author

@chhoumann chhoumann left a comment

Choose a reason for hiding this comment

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

Holding

const imagePath = parseClipboardLocalImagePath(clipboardText);
if (!imagePath) return null;

const req =
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Need to find alternative approach

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Need to simplify

…tter

- Replace window.require('fs') with fetch('file://...') for reading
  local clipboard image paths. This removes the Node.js fs dependency
  and uses a standard web API that works in Electron.
- Simplify completeFormatter clipboard handling: replace depth counter
  (clipboardContentScopeDepth) with a simple boolean flag
  (formattingFileContent), remove withClipboardContentScope wrapper.
- Update tests to mock fetch instead of window.require.

Addresses review comments on PR #1162.

Co-Authored-By: Christian <christian@bagerbach.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 the current code and only fix it if needed.

Inline comments:
In `@src/formatters/completeFormatter.ts`:
- Around line 415-432: The clipboard cache (resolvedClipboardContent) can
persist across formatting runs and return stale clipboard data; ensure the cache
is cleared at the end of a single formatting flow by resetting
this.resolvedClipboardContent = undefined (or null) when the flow completes (for
example in the finalization path of formatFileContent or in the same place that
calls clearDestinationContext()), or scope the cache to a per-run token and
invalidate it when the run finishes so getClipboardContent always reads fresh
clipboard content for each formatFileContent invocation.

In `@src/utilityObsidian.ts`:
- Around line 831-836: absolutePathToFileUrl currently interpolates raw paths
into a file:// string which breaks on characters like #, ?, or %; update
absolutePathToFileUrl to percent-encode the path by building a URL object and
assigning the normalized forward-slash path to URL.pathname (e.g. compute
forwardSlashPath and prefix a leading "/" if needed, then set url.pathname =
that value) and return the URL string (url.toString() or url.href) so the
produced file:// URL is properly encoded before any fetch.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e8439a5d-771c-4326-8e0f-c37d33b84d5e

📥 Commits

Reviewing files that changed from the base of the PR and between b7bad9e and 05b4a26.

📒 Files selected for processing (3)
  • src/formatters/completeFormatter.clipboard.test.ts
  • src/formatters/completeFormatter.ts
  • src/utilityObsidian.ts

Comment on lines 415 to +432
protected async getClipboardContent(): Promise<string> {
if (this.formattingFileContent && this.destinationSourcePath !== null) {
if (
this.resolvedClipboardContent &&
this.resolvedClipboardContent.destinationSourcePath ===
this.destinationSourcePath
) {
return this.resolvedClipboardContent.value;
}

const resolvedContent = await resolveClipboardForNoteContent(
this.app,
this.destinationSourcePath,
);
this.resolvedClipboardContent = {
destinationSourcePath: this.destinationSourcePath,
value: resolvedContent,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope the clipboard cache to a single formatting flow.

resolvedClipboardContent is only cleared when the destination changes or clearDestinationContext() is called. In the provided src/engine/TemplateEngine.ts snippets, the destination context is set before formatFileContent() but never cleared afterward, so a later run against the same note can reuse stale clipboard text/embed instead of rereading the clipboard. Please reset this cache at the end of the active flow, or clear the destination context from the callers’ finally blocks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/formatters/completeFormatter.ts` around lines 415 - 432, The clipboard
cache (resolvedClipboardContent) can persist across formatting runs and return
stale clipboard data; ensure the cache is cleared at the end of a single
formatting flow by resetting this.resolvedClipboardContent = undefined (or null)
when the flow completes (for example in the finalization path of
formatFileContent or in the same place that calls clearDestinationContext()), or
scope the cache to a per-run token and invalidate it when the run finishes so
getClipboardContent always reads fresh clipboard content for each
formatFileContent invocation.

devin-ai-integration[bot]

This comment was marked as resolved.

Encode each path segment with encodeURIComponent to handle special
characters like # and ? that would otherwise be interpreted as URL
fragment/query delimiters.

Co-Authored-By: Christian <christian@bagerbach.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 REQUEST] {{clipboard}} to support image type

1 participant