diff --git a/packages/app/e2e/browser/assistant-selection-copy.spec.ts b/packages/app/e2e/browser/assistant-selection-copy.spec.ts index ad05e2af7..3c9440fdb 100644 --- a/packages/app/e2e/browser/assistant-selection-copy.spec.ts +++ b/packages/app/e2e/browser/assistant-selection-copy.spec.ts @@ -118,11 +118,18 @@ async function selectAssistantText(page: Page, text: string): Promise { await selectAssistantTextRange(page, text, text); } -async function doubleClickAssistantText(page: Page, text: string): Promise { +async function doubleClickAssistantMarkdownText( + page: Page, + tag: "code" | "strong", + text: string, +): Promise { const assistantMessage = page.getByTestId("assistant-message").filter({ hasText: "Direct matches:", }); - await assistantMessage.getByText(text, { exact: true }).dblclick(); + await assistantMessage + .locator(`[data-paseo-markdown-tag="${tag}"]`) + .filter({ hasText: text }) + .dblclick({ position: { x: 5, y: 5 } }); } async function selectAssistantTextRange( @@ -333,13 +340,20 @@ test("copying an assistant selection preserves Markdown structure and links", as expect(clipboard.html).toContain('Right'); expect(clipboard.html).toContain('Center'); - await doubleClickAssistantText(page, "apply_patch"); + await doubleClickAssistantMarkdownText(page, "code", "apply_patch"); await copySelection(page); const doubleClickedCodeClipboard = await readRichClipboard(page); expect(doubleClickedCodeClipboard.plainText).toBe("apply_patch"); expect(doubleClickedCodeClipboard.html).not.toContain(""); + await doubleClickAssistantMarkdownText(page, "strong", "strong prose"); + await copySelection(page); + + const doubleClickedBoldClipboard = await readRichClipboard(page); + expect(doubleClickedBoldClipboard.plainText).toBe("strong"); + expect(doubleClickedBoldClipboard.html).not.toContain(""); + await selectAssistantTextRange( page, "P1 — High-value UI behavior", @@ -395,30 +409,21 @@ test("copying an assistant selection preserves Markdown structure and links", as await copySelection(page); const titledLinkClipboard = await readRichClipboard(page); - expect(titledLinkClipboard.plainText).toBe('[docs](https://docs.example.com "Reference")'); - expect(titledLinkClipboard.html).toContain( - 'docs', - ); + expect(titledLinkClipboard.plainText).toBe("docs"); + expect(titledLinkClipboard.html).not.toContain("https://autolink.example.com', - ); + expect(autolinkClipboard.plainText).toBe("https://autolink.example.com"); await selectAssistantText(page, "workspace file"); await copySelection(page); const fileLinkClipboard = await readRichClipboard(page); - expect(fileLinkClipboard.plainText).toBe("[workspace file](file:///tmp/paseo%20notes.md#L4)"); - expect(fileLinkClipboard.html).toContain( - 'workspace file', - ); + expect(fileLinkClipboard.plainText).toBe("workspace file"); + expect(fileLinkClipboard.html).not.toContain(" { }, ); - it("retains strong delimiters when its complete contents are selected", () => { - const message = mountFixture(); - const element = fixtureElement(message, '[data-paseo-markdown-tag="strong"]'); - const content = createAssistantSelectionClipboardContent( - selectText(element, 0, textNode(element).length), - ); - expect(content?.plainText).toBe("**bold text**"); - expect(content?.html).toContain(""); - }); + it.each(["strong", "em", "s", "h2", "blockquote"])( + "copies all content selected from inside a %s element without its syntax", + (tag) => { + const message = mountFixture(); + const element = fixtureElement(message, '[data-paseo-markdown-tag="strong"]'); + element.setAttribute("data-paseo-markdown-tag", tag); + const content = createAssistantSelectionClipboardContent( + selectText(element, 0, textNode(element).length), + ); + expect(content?.plainText).toBe("bold text"); + expect(content?.html).not.toContain(`<${tag}>`); + }, + ); it("copies complete inline code without delimiters when the selection stays inside", () => { const message = mountFixture(); diff --git a/packages/app/src/assistant-selection-copy/content.web.ts b/packages/app/src/assistant-selection-copy/content.web.ts index ac157039d..6678e4a5b 100644 --- a/packages/app/src/assistant-selection-copy/content.web.ts +++ b/packages/app/src/assistant-selection-copy/content.web.ts @@ -278,6 +278,13 @@ function shouldPreserveSemanticElement(range: Range, element: Element): boolean if (tag === "p" || isTableStructure(tag)) { return true; } + const isSelectableSemantic = tag !== null && tag !== "ol" && tag !== "ul"; + if ( + (isSelectableSemantic || element.tagName === "A") && + selectionStaysInsideElement(range, element) + ) { + return false; + } if (tag === "ol" || tag === "ul") { const selectedItems = selectedListItems(element, range); return selectedItems.some((item) => hasSelectedAllContents(range, item, true)); @@ -285,6 +292,15 @@ function shouldPreserveSemanticElement(range: Range, element: Element): boolean return hasSelectedAllContents(range, element, tag === "li"); } +function selectionStaysInsideElement(range: Range, element: Element): boolean { + return ( + range.startContainer !== element && + range.endContainer !== element && + element.contains(range.startContainer) && + element.contains(range.endContainer) + ); +} + function selectedListItems(list: Element, range: Range): Element[] { return Array.from(list.children).filter( (child) =>