Merge pull request #4349 from nesquena/stage-4339

Release v0.51.472 — paste image + text together (#4339)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-06-16 21:47:26 -07:00
commit fa21a4bb81
3 files changed
+49 -15

No files matched your search

+6
View File
@@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.472] — 2026-06-17 — Release QG (paste image + text together)
### Fixed
- **Pasting an image copied from a browser, Notes, or Slack now attaches the image again (#1620 regression).** When the clipboard carries both text and an image (the normal shape for "right-click → Copy Image" and most rich-source copies), the composer paste handler previously bailed entirely and silently dropped the image. It now attaches any real image (`kind==='file'` image item) while still letting the browser paste accompanying text normally — matching how the major AI chat UIs handle paste. Pure screenshots still attach as before, plain text still pastes with no attachment, and a rich-text editor's rendered HTML preview (a `text/html` string, not a file) is not misclassified as an image. Thanks @kaishi00.
## [v0.51.471] — 2026-06-17 — Release QF (searchable settings)
### Added
+8 -9
View File
@@ -1531,16 +1531,15 @@ document.addEventListener('keydown',async e=>{
});
$('msg').addEventListener('paste',e=>{
const items=Array.from(e.clipboardData?.items||[]);
// When the clipboard carries BOTH text and an image (common from Notes,
// Word, browsers, Slack — the OS attaches a rendered preview alongside
// the plain text), prefer the text and let the browser paste normally.
// Only intercept when the clipboard is image-only (true screenshot paste).
// Tighten the image filter to kind==='file' so string items advertising an
// image MIME (e.g. text/html with an embedded data URI) are not misclassified.
const hasText=items.some(i=>i.kind==='string'&&(i.type==='text/plain'||i.type==='text/html'));
// Extract image items (kind==='file' filter avoids misclassifying text/html
// with embedded data URIs as images).
const imageItems=items.filter(i=>i.kind==='file'&&i.type.startsWith('image/'));
if(!imageItems.length||hasText)return;
e.preventDefault();
if(!imageItems.length)return;
// If text is also present (common when copying images from browsers, Notes,
// Slack, etc.), let the browser paste the text normally AND attach the image.
// Only preventDefault when the clipboard is image-only (true screenshot paste).
const hasText=items.some(i=>i.kind==='string'&&(i.type==='text/plain'||i.type==='text/html'));
if(!hasText)e.preventDefault();
const pasteTs=Date.now();
const files=imageItems.map((i,idx)=>{
const blob=i.getAsFile();
+35 -6
View File
@@ -67,15 +67,44 @@ class TestPasteHandlerTextWithImage:
body,
), "imageItems filter must use kind === 'file' && type.startsWith('image/')"
def test_handler_skips_attach_when_text_present(self):
"""The early-return guard must short-circuit when text is in the clipboard,
so the browser's default text-paste runs and no image is attached."""
def test_handler_skips_preventDefault_when_text_present(self):
"""When text is in the clipboard alongside an image, preventDefault() must
NOT be called so the browser's native text-paste runs. The handler should
still attach the image via addFiles(). The #1620 intent (don't swallow text)
is preserved by gating preventDefault on !hasText instead of bailing entirely.
Additionally, text-only clipboards (no image items) must early-return before
reaching the attach path."""
body = _paste_handler_body()
# Guard shape: if(!imageItems.length || hasText) return;
# Text-only early-out: if no image items, return before any attach logic.
assert re.search(
r"if\s*\(\s*!\s*imageItems\.length\s*\|\|\s*hasText\s*\)\s*return\s*;",
r"if\s*\(\s*!\s*imageItems\.length\s*\)\s*return\s*;",
body,
), "guard must early-return when there are no image files OR text is present"
), "handler must early-return when there are no image files (text-only paste)"
# preventDefault must be gated on !hasText so text+image pastes let the
# browser insert text natively.
assert re.search(
r"if\s*\(\s*!\s*hasText\s*\)\s*e\.preventDefault\s*\(\s*\)\s*;",
body,
), "preventDefault must be gated on !hasText (only intercept image-only paste)"
def test_handler_attaches_image_when_both_text_and_image_present(self):
"""When the clipboard carries both text and image items (e.g. browser
Copy Image), addFiles() must still be reached so the image is attached.
The image attach path must NOT be guarded by hasText."""
body = _paste_handler_body()
# addFiles must appear AFTER the preventDefault gate, reachable regardless
# of whether text is present.
prevent_idx = body.find("preventDefault")
addfiles_idx = body.find("addFiles(files)")
assert addfiles_idx > prevent_idx, (
"addFiles(files) must come after the preventDefault gate and be "
"reachable even when hasText is true"
)
# No guard that bails before addFiles when hasText is true.
assert not re.search(
r"hasText\s*\)\s*return\s*;",
body,
), "handler must NOT early-return on hasText before addFiles (regression for #1620 fix)"
def test_handler_still_intercepts_pure_screenshot_paste(self):
"""Pure-screenshot paste (image-only clipboard) must still call preventDefault()