Skip to content

Conversation

@S-A-Adit
Copy link
Contributor

@S-A-Adit S-A-Adit commented Nov 24, 2025

Summary

This PR fixes an issue where copying images from certain websites fails to paste correctly in Affine. Some pages place both an image file and an HTML snippet containing a blob: URL into the clipboard. The blob: URL is not accessible outside the original page context, causing the paste operation to fail.

What This Change Does

  • Adds a paste event handler to lit-host.ts.
  • Detects and prioritizes actual image files from the clipboard.
  • Prevents default behavior when an image file is present and inserts the file directly via editor:insert-file.
  • Cleans up the event listener on disconnect.

Why This Fix Works

Previously, the paste handler attempted to use a blob: URL from the HTML clipboard data, which is invalid in our environment. By checking for and using the real image file first, Affine can reliably paste images from webpages that include such mixed clipboard content.

Testing

  • Verified paste works from websites that previously failed (e.g., pages that produce both blob: and image file entries).
  • Confirmed existing paste behavior remains unchanged for normal image clipboard data.

This resolves the image copy-paste issue reported in v0.24.1 (web + Chrome + self-hosted).

Summary by CodeRabbit

Release Notes

  • New Features
    • Images can now be pasted directly from clipboard into the editor. When you paste content containing an image file, the image is automatically detected and inserted into the document while maintaining existing paste behavior for other content types.

✏️ Tip: You can customize this high-level summary in your review settings.

Add paste handler to insert image files from clipboard
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

Walkthrough

A paste event handler is introduced to intercept clipboard paste events, detect image files, and route them to the file insertion command system instead of allowing default paste behavior. Event listener registration and cleanup are managed through the component lifecycle hooks.

Changes

Cohort / File(s) Summary
Image Paste Handler
blocksuite/framework/std/src/view/element/lit-host.ts
Added _handlePaste method to detect image files in clipboard paste events and trigger editor:insert-file action. Paste event listener wired during connectedCallback and removed during disconnectedCallback for proper lifecycle management.

Sequence Diagram

sequenceDiagram
    participant User
    participant Browser
    participant LitHost
    participant CommandSystem
    participant Editor

    User->>Browser: Paste (Ctrl+V)
    Browser->>LitHost: paste event
    LitHost->>LitHost: Detect image in clipboard
    alt Image Found
        LitHost->>LitHost: Prevent default behavior
        LitHost->>CommandSystem: Trigger editor:insert-file
        CommandSystem->>Editor: Insert image
    else No Image
        LitHost->>Browser: Allow default paste
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15–25 minutes

  • Verify image detection logic correctly identifies clipboard image data
  • Confirm event listener lifecycle (registration and cleanup) prevents memory leaks
  • Validate integration with the editor:insert-file command system and proper error handling
  • Ensure default paste behavior is preserved when no image is present

Poem

🐰 A carrot from the clipboard flies,
When pasted with a rabbit's eyes—
No text, just images so fine,
Through the command system they align! ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: detecting and prioritizing image files over blob URLs when pasting from clipboard, which directly addresses the paste failure issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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
Contributor

@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 (1)
blocksuite/framework/std/src/view/element/lit-host.ts (1)

121-122: Consider whether stopPropagation() is necessary.

Using both preventDefault() and stopPropagation() prevents other paste event handlers (e.g., from widgets or browser extensions) from processing the event. While this ensures the image file is handled exclusively by your logic, it may interfere with other components that rely on paste events.

If you intend to prevent all other paste handling when an image file is detected, the current implementation is correct. Otherwise, consider removing stopPropagation():

     if (imageFile) {
       e.preventDefault();
-      e.stopPropagation();
       try {

Please confirm this is the intended behavior based on your testing.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1d9fe3b and a59facf.

📒 Files selected for processing (1)
  • blocksuite/framework/std/src/view/element/lit-host.ts (2 hunks)
🔇 Additional comments (2)
blocksuite/framework/std/src/view/element/lit-host.ts (2)

161-161: Event listener registration looks good.

The paste event listener is properly registered on the EditorHost element in connectedCallback, which ensures it only intercepts paste events within the editor context. The arrow function syntax for _handlePaste correctly preserves the this binding.


167-167: Event listener cleanup is properly implemented.

The event listener is correctly removed in disconnectedCallback using the same function reference, preventing memory leaks when the component is disconnected from the DOM.

Comment on lines +107 to +127
private readonly _handlePaste = (e: ClipboardEvent) => {
const files = e.clipboardData?.files;
if (!files || files.length === 0) {
return;
}

// when copy-pasting from some websites, the clipboard may contain both an image file
// and an html string with a `blob:` url. The `blob:` url is not accessible,
// so we should prioritize handling the image file.
const imageFile = Array.from(files).find(file =>
file.type.startsWith('image/')
);

if (imageFile) {
e.preventDefault();
e.stopPropagation();
this.command.execute('editor:insert-file', {
file: imageFile,
});
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add error handling for command execution.

The command.execute call lacks error handling. If the editor:insert-file command fails or throws an exception, it will propagate uncaught and potentially break the paste operation or leave the editor in an inconsistent state.

Consider wrapping the command execution in a try-catch block:

     if (imageFile) {
       e.preventDefault();
       e.stopPropagation();
-      this.command.execute('editor:insert-file', {
-        file: imageFile,
-      });
+      try {
+        this.command.execute('editor:insert-file', {
+          file: imageFile,
+        });
+      } catch (error) {
+        console.error('Failed to insert image file:', error);
+        // Optionally re-enable default paste behavior or show user feedback
+      }
     }

Verify the command API and its availability.

Please confirm that the editor:insert-file command exists in the command registry and accepts a file parameter of type File. Also verify that this command is always registered when the editor is mounted.

Run the following script to verify the command registration and usage:


🏁 Script executed:

#!/bin/bash
# Description: Search for editor:insert-file command registration and usage

# Search for command registration
echo "=== Searching for editor:insert-file command registration ==="
rg -n "editor:insert-file" --type=ts -C3

# Search for command manager execute calls to understand error handling patterns
echo -e "\n=== Searching for command.execute patterns ==="
rg -n "command\.execute\(" --type=ts -A2 -B2 | head -50

Length of output: 1110


I need to verify more about the command system and error handling patterns. Let me search for command initialization and registration.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find where this.command is initialized in lit-host.ts and related files

echo "=== Searching for 'this.command' initialization in lit-host.ts ==="
rg -n "this\.command\s*=" --type=ts -B5 -A5 blocksuite/framework/std/src/view/element/lit-host.ts

echo -e "\n=== Searching for class/interface definition in lit-host.ts ==="
rg -n "export\s+(class|interface)" --type=ts blocksuite/framework/std/src/view/element/lit-host.ts

echo -e "\n=== Searching for command property/field definition ==="
rg -n "command\s*:" --type=ts -B2 -A2 blocksuite/framework/std/src/view/element/lit-host.ts | head -30

echo -e "\n=== Searching for command registration patterns (register) ==="
rg -n "\.register.*command\|registerCommand" --type=ts -C2 | head -50

Length of output: 302



---

🏁 Script executed:

```shell
#!/bin/bash
# Find where command property comes from and error handling patterns

echo "=== Searching for SignalWatcher definition ==="
rg -n "export.*SignalWatcher|class SignalWatcher" --type=ts -A5 | head -30

echo -e "\n=== Searching for 'this.command' usage in codebase (not just lit-host) ==="
rg -n "this\.command\." --type=ts -B2 -A2 | head -50

echo -e "\n=== Searching for command.execute with try-catch or error handling ==="
rg -n "command\.execute" --type=ts -B3 -A3 | grep -E "(try|catch|error|Error|throw)" -C5 | head -40

echo -e "\n=== Searching for EditorHost full class structure ==="
rg -n "export class EditorHost" --type=ts -A30 | head -50

Length of output: 6492



---

🏁 Script executed:

```shell
#!/bin/bash
# Find EditorHost constructor, command initialization, and command definition

echo "=== Searching for EditorHost constructor and command setup ==="
rg -n "constructor|@property|@attribute|command\s*\?:" --type=ts blocksuite/framework/std/src/view/element/lit-host.ts -A2 | head -40

echo -e "\n=== Searching for editor:insert-file command definition ==="
rg -n "editor:insert-file|insert-file" --type=ts -B3 -A3

echo -e "\n=== Searching for command execution in entire codebase with context ==="
rg -n "\.execute\(" --type=ts -B2 -A2 | grep -i "command\|execute" -A3 -B3 | head -60

echo -e "\n=== Searching for command property provider or injection ==="
rg -n "command.*=" --type=ts blocksuite/framework/std/src/view/element/lit-host.ts -B2 -A2

Length of output: 3513



---

🏁 Script executed:

```shell
#!/bin/bash
# Find BlockStdScope definition and command property, check full EditorHost class

echo "=== Searching for BlockStdScope definition ==="
rg -n "export.*BlockStdScope|class BlockStdScope|interface BlockStdScope" --type=ts -A10 | head -40

echo -e "\n=== Searching for command property in BlockStdScope or related types ==="
rg -n "command.*:\s*" --type=ts | grep -i "scope\|std" -B2 -A2 | head -30

echo -e "\n=== Full EditorHost class to find all properties ==="
rg -n "export class EditorHost" --type=ts -A80 | head -100

echo -e "\n=== Search for command registration or creation ==="
rg -n "\.command\s*=|new Command|createCommand" --type=ts -B2 -A2 | head -40

Length of output: 16397



---

🏁 Script executed:

```shell
#!/bin/bash
# Search exhaustively for editor:insert-file command registration or definition

echo "=== Exact search for editor:insert-file with all context ==="
rg -n "editor:insert-file" --type=ts -B5 -A5

echo -e "\n=== Search for insert-file command registration ==="
rg -n "insert-file" --type=ts -B3 -A3

echo -e "\n=== Search for CommandManager.register or .execute implementation ==="
rg -n "class CommandManager|execute\s*\(" --type=ts blocksuite/framework/std/src/command/manager.ts -A10 | head -60

echo -e "\n=== Search for command registration in BlockStdScope ==="
rg -n "BlockStdScope" --type=ts blocksuite/framework/std/src -A5 | head -50

echo -e "\n=== Check if command property has null safety ==="
cat blocksuite/framework/std/src/view/element/lit-host.ts | sed -n '107,135p'

Length of output: 7096



---

🏁 Script executed:

```shell
#!/bin/bash
# Final verification: search for where editor:insert-file command might be registered
# Check if it's in a different package or file structure

echo "=== Search entire repo for insert command registration patterns ==="
rg -n "register.*insert|'insert.*'|\"insert.*\"" --type=ts | grep -i "command\|execute" -B2 -A2 | head -50

echo -e "\n=== Search for any file containing command registrations ==="
rg -n "\.register\(" --type=ts -B1 -A1 | head -40

echo -e "\n=== Check BlockStdScope file for command initialization ==="
fd -n "std-scope\|BlockStdScope" --type=ts | head -5

Length of output: 3079


Add error handling and verify command registration for editor:insert-file.

The command.execute() call lacks error handling, and the editor:insert-file command does not appear to be registered anywhere in the codebase. This will cause unhandled exceptions and broken paste functionality.

  1. Error handling required: Wrap the command execution in try-catch.
  2. Command registration missing: Ensure editor:insert-file command is registered before use.

Without these fixes, the paste handler will fail silently or throw uncaught exceptions.

     if (imageFile) {
       e.preventDefault();
       e.stopPropagation();
-      this.command.execute('editor:insert-file', {
-        file: imageFile,
-      });
+      try {
+        this.command.execute('editor:insert-file', {
+          file: imageFile,
+        });
+      } catch (error) {
+        console.error('Failed to insert image file:', error);
+      }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private readonly _handlePaste = (e: ClipboardEvent) => {
const files = e.clipboardData?.files;
if (!files || files.length === 0) {
return;
}
// when copy-pasting from some websites, the clipboard may contain both an image file
// and an html string with a `blob:` url. The `blob:` url is not accessible,
// so we should prioritize handling the image file.
const imageFile = Array.from(files).find(file =>
file.type.startsWith('image/')
);
if (imageFile) {
e.preventDefault();
e.stopPropagation();
this.command.execute('editor:insert-file', {
file: imageFile,
});
}
};
private readonly _handlePaste = (e: ClipboardEvent) => {
const files = e.clipboardData?.files;
if (!files || files.length === 0) {
return;
}
// when copy-pasting from some websites, the clipboard may contain both an image file
// and an html string with a `blob:` url. The `blob:` url is not accessible,
// so we should prioritize handling the image file.
const imageFile = Array.from(files).find(file =>
file.type.startsWith('image/')
);
if (imageFile) {
e.preventDefault();
e.stopPropagation();
try {
this.command.execute('editor:insert-file', {
file: imageFile,
});
} catch (error) {
console.error('Failed to insert image file:', error);
}
}
};
🤖 Prompt for AI Agents
In blocksuite/framework/std/src/view/element/lit-host.ts around lines 107 to
127, the paste handler calls this.command.execute('editor:insert-file', ...)
without error handling and the command may not be registered; wrap the execute
call in a try-catch to handle/log errors and prevent uncaught exceptions, and
before invoking ensure the command is registered (either by checking
this.command.has('editor:insert-file') or registering a no-op/handler during
component initialization); if the command is missing, either register the
handler that inserts the file or fall back to an alternative paste behavior and
log a clear warning.

@darkskygit darkskygit requested review from EYHN, L-Sun and zzj3720 November 27, 2025 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant