Repository navigation
feat(muya): add clipboardFilePath paste hook - #4398
Conversation
Port the legacy @muyajs `clipboardFilePath` option to @muyajs/core. When the user pastes and the OS clipboard holds a file (e.g. an image copied from a file manager rather than image bytes), the embedder resolves it to a local path; if that path is a non-empty image file, muya inserts it as an inline image at the cursor instead of running the default text/HTML paste. Returning '' (or omitting the hook) preserves all existing paste behaviour. - Add optional `clipboardFilePath?: () => Promise<string>` to IMuyaOptions. - Add a pure `resolveClipboardImagePath` helper in utils/paste that gates the hook result on IMAGE_EXT_REG (matching the legacy pasteImage guard). - Short-circuit the clipboard paste flow when the hook yields an image path, splicing `` into the anchor block (src escaped like Format.replaceImage) and placing the cursor after it. - Tests: unit-cover resolveClipboardImagePath and exercise pasteHandler end to end (hook invoked, image inserted, escaping, and fall-through when the hook returns '' / a non-image / is absent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR ports the legacy clipboardFilePath paste hook into @muyajs/core (packages/muya) so embedders (e.g., the desktop app) can resolve “clipboard contains a file” pastes to a local image path and insert it as an inline markdown image.
Changes:
- Adds
clipboardFilePath?: () => Promise<string>toIMuyaOptions. - Introduces
resolveClipboardImagePath()helper (pure + unit-tested) to validate the hook output againstIMAGE_EXT_REG. - Integrates the hook into
Clipboard.pasteHandlerto insertand short-circuit the normal paste flow, plus adds end-to-end tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/muya/src/utils/paste.ts | Adds resolveClipboardImagePath() helper used to validate hook output as an image path. |
| packages/muya/src/utils/tests/getCopyTextType.spec.ts | Adds unit coverage for resolveClipboardImagePath(). |
| packages/muya/src/types.ts | Extends IMuyaOptions with the optional clipboardFilePath hook and docs. |
| packages/muya/src/clipboard/index.ts | Calls the hook during paste and inserts an inline image markdown string when it resolves to an image path. |
| packages/muya/src/clipboard/tests/clipboardFilePath.spec.ts | Adds end-to-end pasteHandler tests for the new hook behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Good catch — fixed in afa0d31. pasteHandler now snapshots text/plain and text/html from event.clipboardData synchronously at the top, before any await. The clipboardFilePath hook resolution (the first await) now happens only after that snapshot, so the DataTransfer is never read once it may have been detached. The captured values are threaded through the !isSelectionInSameBlock recursion via optional rawText/rawHtml params (mirroring the legacy @muyajs pasteHandler(event, type, rawText, rawHtml) signature), so the re-entry doesn't re-read a possibly-detached clipboard either. Behaviour is unchanged: the file-path hook still takes precedence and inserts an image, and normal text/HTML paste still works when the hook is absent or resolves to ''/non-image. Added a regression test ('pastes the snapshotted text/plain when the hook is present but returns ""') that fails on the old ordering, and updated the precedence test to reflect that clipboardData is now read synchronously up front.
`pasteHandler` awaited `resolveClipboardImagePath()` (the async `clipboardFilePath` hook) before reading `event.clipboardData.getData()`. Because the DOM paste listener does not await the handler, execution yields at that first `await` and resumes after event dispatch completes — at which point `event.clipboardData` can be detached and `getData()` returns ''. This silently broke normal text/HTML paste whenever `clipboardFilePath` was configured but resolved to '' / a non-image. Snapshot text/html synchronously at the top of `pasteHandler`, before any `await`, and thread the snapshot through the `!isSelectionInSameBlock` recursion via optional `rawText`/`rawHtml` params so the re-entry never reads a detached clipboard. Mirrors the legacy `@muyajs` `pasteHandler(event, type, rawText, rawHtml)` ordering. Behaviour is unchanged: the file-path hook still takes precedence and inserts an image, and normal paste still works when the hook is absent/empty. Add a regression test proving text/plain still pastes when the hook is present-but-returns-''; update the precedence test to reflect that clipboardData is now read synchronously up front. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Why
When the user pastes and the OS clipboard holds a file (e.g. an image copied in a file manager rather than image bytes), the desktop app resolves it to a path and inserts an image. The legacy
packages/muyajsengine accepted aclipboardFilePathoption (() => Promise<string>returning the file path or'') for exactly this.@muyajs/core(packages/muya) had no such hook, which blocks the desktop migration off the legacy engine.This ports that hook to
@muyajs/core.What
IMuyaOptions: add optionalclipboardFilePath?: () => Promise<string>.utils/paste.ts: add a pure, testableresolveClipboardImagePath(hook)helper. It calls the hook and returns the resolved path only when it is a non-empty string whose extension matchesIMAGE_EXT_REG— mirroring the legacypasteImageguard. Otherwise returns''.clipboard/index.ts(pasteHandler): right after the selection/clipboard-data guard (matching the legacy order where the image path is checked first), consult the hook. If it yields an image path, spliceinto the anchor block at the cursor (src escaped the same way asFormat.replaceImage, so spaces and#survive) and return early. Inline images in muya are markdown text on a content block, so this renders as an inline image.Behavior
'', or returns a non-image path → unchanged: the normal text/HTML paste runs exactly as before..png/.jpg/.jpeg/.gif/.svg/.webp, case-insensitive, query string tolerated) → that path is inserted as an inline image at the cursor, short-circuiting the text/HTML paste.src/muya.tsis intentionally untouched.Tests
utils/__tests__/getCopyTextType.spec.ts: unit-coversresolveClipboardImagePath(no hook, image path, every supported extension, query string, empty string, non-image).clipboard/__tests__/clipboardFilePath.spec.ts(new): drivespasteHandlerend to end with a mockedclipboardFilePath— asserts the hook is invoked, the resolved path is inserted asat the cursor, src escaping, the short-circuit (text/HTMLgetDatanever read), and fall-through when the hook returns''/ a non-image / is absent.normalizePastedHTMLis partially mocked in the fall-through cases since its DOMPurify call needs a DOM the defaultnodetest env doesn't provide.Verify (all green)
lintlint:typescheck-circulartesttest:spec🤖 Generated with Claude Code