feat(prompt): paste clipboard images into value prompts - #1492
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
📝 WalkthroughWalkthroughThis PR adds clipboard-image pasting for value prompts, saves pasted images as vault attachments, inserts embed links, wires the behavior into prompt modals and formatter flows, and gates it away from path/file-target prompts using path-context scanning. ChangesClipboard image paste feature
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant PromptInput
participant attachImagePasteHandler
participant saveClipboardImageToVault
participant PromptModal
User->>PromptInput: paste image
PromptInput->>attachImagePasteHandler: paste event
attachImagePasteHandler->>saveClipboardImageToVault: save image attachment
saveClipboardImageToVault-->>attachImagePasteHandler: created TFile
attachImagePasteHandler->>PromptInput: insert embed link
User->>PromptModal: submit
PromptModal->>attachImagePasteHandler: isBusy()/whenIdle()
attachImagePasteHandler-->>PromptModal: busy or idle
PromptModal->>PromptModal: defer or finish submit
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Deploying quickadd with
|
| Latest commit: |
f94e0e4
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://b07953f2.quickadd.pages.dev |
| Branch Preview URL: | https://chhoumann-issue-1484-clipboa.quickadd.pages.dev |
Split the {{CLIPBOARD}} image fallback's save/link logic out of
CaptureChoiceFormatter into src/utils/clipboardImageAttachments.ts so the
prompt-input paste feature (#1484) can reuse it instead of duplicating it.
Save and link generation are separate steps so callers can record the created
file for rollback before linking can fail. The saver now also refuses
attachment paths that escape the vault boundary (defense in depth at the
write sink).
Pasting an image into a QuickAdd value prompt now saves it as a vault attachment (via Obsidian's attachment-folder settings) and inserts an embed link at the caret. Closes the interactive half of #1484 (discussions #609, #1100); the {{CLIPBOARD}} token half shipped in 2.14.0. Semantics: - Clipboard text always wins (untrimmed check, parity with the shipped {{CLIPBOARD}} image fallback); the image path only runs when text/plain is empty. - Gated by value SINK, not call site: prompts opened while formatting note content accept image paste; prompts opened from path passes (file name, folder, template path, capture destination, location targets) and number/slider prompts never do - an embed link would corrupt a path. - Files are extracted from the DataTransfer synchronously (Chromium neuters it after the handler yields), saved strictly sequentially (attachment-path dedupe only sees landed files), and the input is frozen during the save so the caret cannot go stale. Submit during a save defers until the embed is inserted, so Ctrl+V-then-Enter keeps the image. - Insertion goes through execCommand('insertText') (undo-integrated, popout-safe) with a setRangeText fallback. - Standard web APIs only: works where the webview supports image paste and degrades to a silent no-op elsewhere (mobile-safe, no Electron).
One-page form text/textarea (and default-case) fields now accept clipboard image paste via the shared handler. FieldRequirement carries path-context provenance recorded during collection: any occurrence of a variable in a file name format, folder path, capture target, insert-after/before target, or template path marks the field sticky as path context, and such fields never offer image paste - the same value lands in a path, where an embed link would corrupt it. Templates included FROM a path string inherit path context. Submit defers while a pasted image is still saving, and all paste handlers detach on close.
Adversarial implementation review (2 opposite-model + 1 same-model) findings:
- Template-inclusion memo is now keyed per scan context: a template first
scanned from capture content and later reached from a path string
(insert-after target, file name) is re-walked so its variables lose image
paste - the ref-only memo silently kept them pastable (must-fix).
- {{MVALUE}} requirements are path-tainted like VALUE ones, including the
cached-singleton textual re-check.
- Deferred submits (Enter during an in-flight save) are guarded by didClose
in both prompt classes: cancelling before the save lands no longer fires a
spurious submit on the closed modal.
- The pasted-image save promise can never reject: link-insertion failures
are caught and noticed, so whenIdle()-deferred submits cannot be dropped.
- The DataTransferItem's declared MIME drives the extension (getAsFile() can
return a File with an empty .type).
- Null-prototype MIME map: a hostile type like 'constructor' can no longer
hit inherited Object.prototype members in index lookups.
- Vault saves are serialized across handlers, so same-second pastes into two
one-page fields cannot collide on the attachment path.
52f03b0 to
ab7e4b3
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/gui/imagePasteHandler.ts (1)
85-134: 🚀 Performance & Scalability | 🔵 TrivialNo timeout/circuit-breaker on the vault write; a stuck save blocks the caller indefinitely.
saveAndInsertawaitssaveClipboardImageToVault(and the serializedenqueueVaultSavequeue) with no timeout. Ifapp.vault.createBinaryorgetAvailablePathForAttachmentever hangs (adapter/disk issue),pendingSavenever resolves,isBusy()staystrueforever, and downstream consumers likeOnePageInputModal.submit()(which doesbusyHandle.whenIdle().then(() => this.submit())) will wait indefinitely with no way for the user to force a submit.This is a rare failure mode on desktop Obsidian, so treat as optional hardening rather than a blocker.
Also applies to: 167-172
🤖 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/gui/imagePasteHandler.ts` around lines 85 - 134, The save path in saveAndInsert can hang indefinitely because enqueueVaultSave and saveClipboardImageToVault are awaited with no timeout. Add a bounded timeout/circuit-breaker around the queued vault write so a stuck app.vault.createBinary or getAvailablePathForAttachment call resolves or fails cleanly, then make sure pendingSave/isBusy can recover and the error path in saveAndInsert logs and notifies the user. Also apply the same hardening to the downstream submit flow that waits on busyHandle.whenIdle so it cannot block forever.src/gui/imagePasteHandler.test.ts (1)
98-118: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant/tautological assertion.
The expected string on Lines 110-114 is constructed by slicing
input.valueitself, so this assertion is self-referential and passes as long as the prefix/suffix literals match - it verifies nothing beyond what the regex on Line 115-117 already checks. Consider dropping the first assertion to avoid giving false confidence about coverage.♻️ Suggested simplification
- expect(input.value).toBe( - "before ![[attachments/Clipboard image" + - input.value.slice("before ![[attachments/Clipboard image".length, input.value.indexOf("]]") + 2) + - "after", - ); expect(input.value).toMatch( /^before !\[\[attachments\/Clipboard image .*\.png\]\]after$/, );🤖 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/gui/imagePasteHandler.test.ts` around lines 98 - 118, The test in imagePasteHandler.test is asserting a self-referential string built from input.value, which adds no real coverage beyond the regex check. Remove the redundant expectation in the attachImagePasteHandler paste test and keep the meaningful assertion that verifies the pasted embed format after flushSaves, so the test relies on an independent expected pattern instead of the current tautological slice-based comparison.src/gui/GenericWideInputPrompt/GenericWideInputPrompt.ts (1)
155-162: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftDuplicate submit-guard and wiring logic across prompt modals.
This wiring block and the
submit()busy-deferral/didCloseguard (including the identical comment text) are copy-pasted verbatim fromGenericInputPrompt.ts(Lines 170-177, 251-260). Given the PR objectives note a follow-up "hardening" pass was already needed for race/boundary fixes, keeping this logic duplicated acrossGenericInputPrompt,GenericWideInputPrompt, and (per tests)OnePageInputModalincreases the risk that a future fix lands in one file but not the others.Consider extracting a small shared helper/mixin (e.g.
wireImagePaste(app, el, options)returning the handle, and adeferSubmitIfBusy(handle, submit)guard) that all three modals call.Also applies to: 237-246
🤖 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/gui/GenericWideInputPrompt/GenericWideInputPrompt.ts` around lines 155 - 162, The image-paste wiring and submit busy/didClose guard logic in GenericWideInputPrompt are duplicated from GenericInputPrompt and mirrored by OnePageInputModal, so future fixes can drift across modal implementations. Extract shared helpers for the image paste attachment and submit deferral/close guard, then update GenericWideInputPrompt to use those helpers so the behavior stays consistent across all prompt modals.
🤖 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.
Nitpick comments:
In `@src/gui/GenericWideInputPrompt/GenericWideInputPrompt.ts`:
- Around line 155-162: The image-paste wiring and submit busy/didClose guard
logic in GenericWideInputPrompt are duplicated from GenericInputPrompt and
mirrored by OnePageInputModal, so future fixes can drift across modal
implementations. Extract shared helpers for the image paste attachment and
submit deferral/close guard, then update GenericWideInputPrompt to use those
helpers so the behavior stays consistent across all prompt modals.
In `@src/gui/imagePasteHandler.test.ts`:
- Around line 98-118: The test in imagePasteHandler.test is asserting a
self-referential string built from input.value, which adds no real coverage
beyond the regex check. Remove the redundant expectation in the
attachImagePasteHandler paste test and keep the meaningful assertion that
verifies the pasted embed format after flushSaves, so the test relies on an
independent expected pattern instead of the current tautological slice-based
comparison.
In `@src/gui/imagePasteHandler.ts`:
- Around line 85-134: The save path in saveAndInsert can hang indefinitely
because enqueueVaultSave and saveClipboardImageToVault are awaited with no
timeout. Add a bounded timeout/circuit-breaker around the queued vault write so
a stuck app.vault.createBinary or getAvailablePathForAttachment call resolves or
fails cleanly, then make sure pendingSave/isBusy can recover and the error path
in saveAndInsert logs and notifies the user. Also apply the same hardening to
the downstream submit flow that waits on busyHandle.whenIdle so it cannot block
forever.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 074a1096-998a-4df6-8b8e-08e82568c1c5
📒 Files selected for processing (19)
docs/docs/FormatSyntax.mddocs/docs/QuickAddAPI.mdsrc/formatters/captureChoiceFormatter.tssrc/formatters/completeFormatter.imagePaste.test.tssrc/formatters/completeFormatter.tssrc/gui/GenericInputPrompt/GenericInputPrompt.tssrc/gui/GenericWideInputPrompt/GenericWideInputPrompt.test.tssrc/gui/GenericWideInputPrompt/GenericWideInputPrompt.tssrc/gui/imagePasteHandler.test.tssrc/gui/imagePasteHandler.tssrc/preflight/OnePageInputModal.test.tssrc/preflight/OnePageInputModal.tssrc/preflight/RequirementCollector.tssrc/preflight/collectChoiceRequirements.test.tssrc/preflight/collectChoiceRequirements.tssrc/styles.csssrc/types/inputPrompt.tssrc/utils/clipboardImageAttachments.test.tssrc/utils/clipboardImageAttachments.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/gui/imagePasteHandler.test.ts (1)
109-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTautological assertion adds no coverage.
The
expect(input.value).toBe(...)at Line 110 builds its expected value from slices ofinput.valueitself, so it passes as long as the value starts with the fixed prefix and contains"]]"— it can't actually catch a wrong middle segment. ThetoMatchregex right after it already validates the real shape.♻️ Proposed simplification
- expect(input.value).toBe( - "before ![[attachments/Clipboard image" + - input.value.slice("before ![[attachments/Clipboard image".length, input.value.indexOf("]]") + 2) + - "after", - ); expect(input.value).toMatch( /^before !\[\[attachments\/Clipboard image .*\.png\]\]after$/, );🤖 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/gui/imagePasteHandler.test.ts` around lines 109 - 117, The assertion in imagePasteHandler.test.ts is tautological because it reconstructs the expected string from input.value itself, so it does not add real coverage. Remove the self-referential expect(input.value).toBe(...) near the createBinary check and rely on the existing shape validation, or replace it with a fixed expected string/assertion that does not depend on input.value. Use the surrounding image paste test setup and input.value checks to keep the intent of the test clear.
🤖 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.
Nitpick comments:
In `@src/gui/imagePasteHandler.test.ts`:
- Around line 109-117: The assertion in imagePasteHandler.test.ts is
tautological because it reconstructs the expected string from input.value
itself, so it does not add real coverage. Remove the self-referential
expect(input.value).toBe(...) near the createBinary check and rely on the
existing shape validation, or replace it with a fixed expected string/assertion
that does not depend on input.value. Use the surrounding image paste test setup
and input.value checks to keep the intent of the test clear.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 91b00cb2-899e-4df2-9ae8-bf5e636f7eeb
📒 Files selected for processing (19)
docs/docs/FormatSyntax.mddocs/docs/QuickAddAPI.mdsrc/formatters/captureChoiceFormatter.tssrc/formatters/completeFormatter.imagePaste.test.tssrc/formatters/completeFormatter.tssrc/gui/GenericInputPrompt/GenericInputPrompt.tssrc/gui/GenericWideInputPrompt/GenericWideInputPrompt.test.tssrc/gui/GenericWideInputPrompt/GenericWideInputPrompt.tssrc/gui/imagePasteHandler.test.tssrc/gui/imagePasteHandler.tssrc/preflight/OnePageInputModal.test.tssrc/preflight/OnePageInputModal.tssrc/preflight/RequirementCollector.tssrc/preflight/collectChoiceRequirements.test.tssrc/preflight/collectChoiceRequirements.tssrc/styles.csssrc/types/inputPrompt.tssrc/utils/clipboardImageAttachments.test.tssrc/utils/clipboardImageAttachments.ts
🚧 Files skipped from review as they are similar to previous changes (15)
- docs/docs/QuickAddAPI.md
- src/types/inputPrompt.ts
- src/utils/clipboardImageAttachments.ts
- src/formatters/captureChoiceFormatter.ts
- src/styles.css
- src/gui/GenericWideInputPrompt/GenericWideInputPrompt.test.ts
- src/gui/GenericWideInputPrompt/GenericWideInputPrompt.ts
- src/gui/GenericInputPrompt/GenericInputPrompt.ts
- src/preflight/collectChoiceRequirements.test.ts
- src/formatters/completeFormatter.imagePaste.test.ts
- src/preflight/OnePageInputModal.test.ts
- src/preflight/collectChoiceRequirements.ts
- src/preflight/OnePageInputModal.ts
- src/utils/clipboardImageAttachments.test.ts
- src/gui/imagePasteHandler.ts
CodeRabbit nitpick: the toBe expectation rebuilt its expected string from input.value itself, adding nothing over the regex assertion below it. The other two nitpicks are deliberate skips: a timeout/circuit-breaker on the vault write guards a failure mode (hung vault adapter) where the whole app is already broken and cancel remains available (only submit defers on a busy save); and the prompt-modal wiring duplication follows the existing deliberate mirror structure between GenericInputPrompt and GenericWideInputPrompt.
# [2.18.0](2.17.2...2.18.0) (2026-07-07) ### Bug Fixes * **userscript:** explain loader failures ([#1483](#1483)) ([71f1a4b](71f1a4b)) ### Features * **ai:** modernize provider model handling end to end ([#1494](#1494)) ([427632e](427632e)) * **append-link:** keep the selected text as the link display text ([#1491](#1491)) ([2bf6dc4](2bf6dc4)), closes [#1479](#1479) [#640](#640) [1455/#1462](#1462) * **format:** {{FOLDERCURRENT}} token for the active file's folder ([#1490](#1490)) ([cd1b9aa](cd1b9aa)), closes [#1358](#1358) [#1480](#1480) [#214](#214) * **prompt:** paste clipboard images into value prompts ([#1492](#1492)) ([61343a0](61343a0)), closes [#1484](#1484) [#1484](#1484) [#609](#609) [#1100](#1100)
|
🎉 This PR is included in version 2.18.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Closes #1484. Motivated by discussions #609 and #1100.
What
Pasting an image (Ctrl/Cmd+V) into a QuickAdd value prompt now saves it as a vault attachment and inserts an embed link at the caret. Works in the single-line prompt, the wide (multiline) prompt, and one-page form text/textarea fields. You can mix typed text with pasted images in one value, and paste several images at once.
The
{{CLIPBOARD}}image fallback (the token half of this ask) already shipped in 2.14.0 (#1393); this PR delivers the interactive half - #609's literal ask ("paste a screenshot into the capture prompt").Design
Full design doc was ultracode-reviewed (3 lenses + 2 adversarial reviewers) before coding; all blocking findings are incorporated.
formatFileContentis the only content pass inCompleteFormatter(every path pass - file name, folder, template path, location targets - callsformat()directly), so image paste is enabled exactly while that pass runs. A{{VALUE:x}}prompt raised while resolvingJournal/{{VALUE:x}}.mdnever offers paste; the same variable prompted for note content does. The one-page form gets the same rule via sticky path-context provenance recorded during requirement collection (any occurrence in a file name format, folder path, capture target, insert-after/before target, or template path taints the field; templates included from a path string inherit path context).text/plain(untrimmed - byte parity with the shipped{{CLIPBOARD}}precedence) leaves the paste to the default handler. Browser image copies carrytext/html+image/pngwith notext/plain, so they paste as images; copying a file in Finder/Explorer pastes its path as text (documented).DataTransfersynchronously (Chromium neuters it once the handler yields), saved strictly sequentially (the attachment-path dedupe only sees landed files - two same-second screenshots would otherwise collide), and the input is frozen (readOnly+ busy cue) during the save so the caret cannot go stale. Submit during a save defers until the embed is inserted, so Ctrl+V-then-Enter keeps the image. A second paste mid-save gets a Notice instead of a silent drop. IME composition is respected.execCommand('insertText')(undo-integrated, firesinputnatively, popout-safe viaownerDocument) with asetRangeText+ syntheticinputfallback.app.fileManager.getAvailablePathForAttachment(honors the user's attachment settings, never a hardcoded folder) + the shippedClipboard image YYYY-MM-DD HH.mm.ss.<ext>convention. Links viagenerateMarkdownLinkwith a forced!prefix.sourcePathis the capture destination when known, else""(vault-root links that resolve from anywhere - never a guess like the active file, which would break relative-link vaults).{{CLIPBOARD}}fallback's rollback-on-abort is unchanged - those files are created invisibly mid-format, so rollback is safe there.ClipboardEvent.clipboardData), no Electron, noPlatformgating - works where the webview supports image paste and degrades to a silent no-op elsewhere.src/utils/clipboardImageAttachments.ts(shared with the capture fallback) and now refuses attachment paths that escape the vault boundary (escapesVaultBoundary), hardening the shipped path too.Accepted residuals (deliberate)
{{VALUE}}) leaves the attachment - same as Obsidian editor paste.{{CLIPBOARD}}fallback (only observable with an attachment folder literally named with{{TOKEN}}text).quickAddApi.inputPrompt/wideInputPromptaccept the newimagePasteoption (documented) sinceInputPromptOptionspasses through verbatim; scripts must opt in.Out of scope (possible follow-ups):
{{CLIPBOARD}}image fallback for Template choices; drag-and-drop of image files onto prompts.Validation
Unit: 30+ new tests (paste handler semantics incl. races/partial failure/IME, saver boundary guard, sink-context gating incl. flag restoration, provenance collection incl. dual-use and template-include inheritance, one-page wiring/defer/teardown). Full suite green (3,600+ tests), lint, typecheck, build.
Live e2e (isolated vault, this branch's build, synthetic
ClipboardEventwith real PNGFiles; zero runtime errors captured across the session):inbox.mdgot- screenshot: ![[Clipboard image 2026-07-06 22.34.03.png]]; input frozen during save;verified:true{{VALUE:topic}}prompt forJournal/{{VALUE:topic}}.md(path context){{VALUE:body}}, content)attachments/(did not exist)... .png,... 1.png), newline-joined, folder auto-created, ctrl+Enter submittedtopicfield inert,bodyfield accepts; cancel clean{{CLIPBOARD}}image fallback after the refactorclip: ![[...]]) - regression-freeAdversarial implementation review (2 opposite-model + 1 same-model reviewers) ran before this PR; all findings (one must-fix: the template-inclusion memo dropped later path-context taint; plus deferred-submit-after-cancel guards, never-rejecting save promise, item-MIME carry, null-prototype MIME map, cross-field save serialization,
{{MVALUE}}taint) are fixed in the final commit, each re-verified live.Summary by CodeRabbit
inputPromptoptionsnow supportsimagePasteto enable the same behavior.