Compare commits

...
Author SHA1 Message Date
paseo-bot[bot]andBae Seongmin 2291f54dc5 fix(app): paste copied text and its image together
Pasting from Word put only an image of the text in the composer, because
the paste handler cancelled the paste whenever the clipboard held an
image. Let the text paste normally when the clipboard has text, and still
attach the images, so content copied with both (Word, or the desktop
browser's element screenshot) keeps both.

Fixes #3851

Co-authored-by: Bae Seongmin <83870598+3ae3ae@users.noreply.github.com>
2026-10-05 11:15:59 +02:00
Bae Seongmin 5eaf6d5464 Merge branch 'main' into fix/paste-text-with-image-fallback 2026-09-29 23:25:10 +09:00
3ae3ae 6dc17b2f1b Merge main into fix/paste-text-with-image-fallback 2026-09-29 16:05:21 +09:00
3ae3ae 052974a308 fix(app): preserve text when the clipboard also contains an image 2026-09-29 15:43:16 +09:00
3 changed files with 66 additions and 2 deletions
@@ -18,6 +18,7 @@ import {
expectAttachButtonDisabled,
fillComposerDraft,
dropFileOnComposer,
pasteClipboardIntoComposer,
sendDraftToQueue,
expectQueuedMessageButton,
startRunningMockAgent,
@@ -53,6 +54,43 @@ const TEST_JSON = {
};
test.describe("Composer attachments", () => {
test("pasting copied document text keeps the text and attaches its image", async ({
page,
context,
withWorkspace,
}) => {
const workspace = await withWorkspace({ prefix: "paste-document-" });
await workspace.navigateTo();
await clickNewChat(page);
await expectComposerVisible(page);
const text = "Copied paragraph with 한글.\nSecond line.";
await pasteClipboardIntoComposer(page, context, {
text,
html: "<p>Copied paragraph with 한글.</p><p>Second line.</p>",
png: MINIMAL_PNG,
});
await expectComposerDraft(page, text);
await expectAttachmentPill(page, "composer-image-attachment-pill");
});
test("pasting an image without text attaches it and leaves the draft empty", async ({
page,
context,
withWorkspace,
}) => {
const workspace = await withWorkspace({ prefix: "paste-image-" });
await workspace.navigateTo();
await clickNewChat(page);
await expectComposerVisible(page);
await pasteClipboardIntoComposer(page, context, { png: MINIMAL_PNG });
await expectAttachmentPill(page, "composer-image-attachment-pill");
await expectComposerDraft(page, "");
});
test("selected file shows a loading attachment until upload is acknowledged", async ({
page,
withWorkspace,
+25 -1
View File
@@ -1,4 +1,4 @@
import { expect, type Page } from "@playwright/test";
import { expect, type BrowserContext, type Page } from "@playwright/test";
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
import { createTempGitRepo } from "./workspace";
import { connectSeedClient, type SeedDaemonClient } from "./seed-client";
@@ -172,6 +172,30 @@ export async function dropFileOnComposer(
await dataTransfer.dispose();
}
/** Puts the given formats on the system clipboard together, then pastes them into the composer with the keyboard shortcut. */
export async function pasteClipboardIntoComposer(
page: Page,
context: BrowserContext,
clipboard: { text?: string; html?: string; png?: Buffer },
): Promise<void> {
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await page.evaluate(
async ({ text, html, pngBase64 }) => {
const formats: Record<string, Blob> = {};
if (text !== undefined) formats["text/plain"] = new Blob([text], { type: "text/plain" });
if (html !== undefined) formats["text/html"] = new Blob([html], { type: "text/html" });
if (pngBase64 !== undefined) {
const bytes = Uint8Array.from(atob(pngBase64), (char) => char.charCodeAt(0));
formats["image/png"] = new Blob([bytes], { type: "image/png" });
}
await navigator.clipboard.write([new ClipboardItem(formats)]);
},
{ text: clipboard.text, html: clipboard.html, pngBase64: clipboard.png?.toString("base64") },
);
await composerInput(page).click();
await page.keyboard.press("ControlOrMeta+v");
}
/** Hover to reveal the X button (hidden until hover on desktop web), then click by accessible label. */
export async function removeAttachmentPill(
page: Page,
+3 -1
View File
@@ -475,7 +475,9 @@ function usePasteImagesEffect(args: PasteImagesEffectArgs): void {
const imageFiles = collectImageFilesFromClipboardData(event.clipboardData);
if (imageFiles.length === 0) return;
event.preventDefault();
// Copied text often carries an image of itself, as from Word. Let the
// text paste normally and attach the images alongside it.
if (!event.clipboardData?.getData("text/plain")) event.preventDefault();
void filesToImageAttachments(imageFiles)
.then((pastedAttachments) => {