import type { BrowserContext } from "@playwright/test"; import { expect, test, type Page } from "../support/fixtures"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; const ASSISTANT_MARKDOWN = [ "Direct matches:", "", "Formatted **strong prose**, _emphasized prose_, and ~~struck prose~~.", "", "- **[First issue](https://example.com/issues/1)**: exact `apply_patch` failure.", "- [Second issue](https://example.com/issues/2): repeated sandbox setup.", "", "5. Fifth item", "6. Sixth item", "7. Seventh item", "8. Eighth item", "", "Nested list:", "", "3. Outer three", "", " 7. Inner seven", " 8. Inner eight", " 9. Inner nine", "", "A hard break ", "stays hard.", "", "www.example.com stays plain Markdown text.", "", '[docs](https://docs.example.com "Reference") and .', "", "[workspace file](file:///tmp/paseo%20notes.md#L4)", "", "`https://example.com/generated` stays code, not a generated link.", "", "```typescript", 'const answer = "yes";', "```", "", "````text", "before", "```", "after", "````", "", "| Left | Right | Center |", "| :-- | --: | :-: |", "| Current \\| active | ~~obsolete~~ | ready |", ].join("\n"); const EXPECTED_WHOLE_SELECTION_MARKDOWN = ASSISTANT_MARKDOWN.replace( "", "[https://autolink.example.com](https://autolink.example.com)", ).replace( "3. Outer three\n\n 7. Inner seven\n 8. Inner eight\n 9. Inner nine", "3. Outer three\n 7. Inner seven\n 8. Inner eight\n 9. Inner nine", ); interface ClipboardContent { html: string; plainText: string; } async function allowRichClipboard(context: BrowserContext): Promise { await context.grantPermissions(["clipboard-read", "clipboard-write"]); } async function selectAssistantMessage(page: Page): Promise { const assistantMessage = page.getByTestId("assistant-message").filter({ hasText: "Direct matches:", }); await expect(assistantMessage).toBeVisible(); await assistantMessage.evaluate((element) => { const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(element); selection?.removeAllRanges(); selection?.addRange(range); }); } async function selectAssistantText(page: Page, text: string): Promise { await selectAssistantTextRange(page, text, text); } async function selectAssistantTextRange( page: Page, startText: string, endText: string, ): Promise { const assistantMessage = page.getByTestId("assistant-message").filter({ hasText: "Direct matches:", }); await assistantMessage.evaluate( (element, selectedRange) => { const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); let startNode: Node | null = null; let startOffset = -1; let endNode: Node | null = null; let endOffset = -1; let textNode = walker.nextNode(); while (textNode) { if (!startNode) { const offset = textNode.textContent?.indexOf(selectedRange.startText) ?? -1; if (offset >= 0) { startNode = textNode; startOffset = offset; } } if (startNode) { const offset = textNode.textContent?.indexOf(selectedRange.endText) ?? -1; if (offset >= 0) { endNode = textNode; endOffset = offset + selectedRange.endText.length; break; } } textNode = walker.nextNode(); } if (!startNode || !endNode) { throw new Error( `Could not find assistant text range: ${selectedRange.startText} — ${selectedRange.endText}`, ); } const range = document.createRange(); range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); const selection = window.getSelection(); selection?.removeAllRanges(); selection?.addRange(range); }, { startText, endText }, ); } async function copySelection(page: Page): Promise { await page.keyboard.press("ControlOrMeta+c"); } async function readRichClipboard(page: Page): Promise { return page.evaluate(async () => { const items = await navigator.clipboard.read(); const item = items[0]; if (!item) { throw new Error("Expected clipboard content"); } return { html: await (await item.getType("text/html")).text(), plainText: await (await item.getType("text/plain")).text(), }; }); } test("copying an assistant selection preserves Markdown structure and links", async ({ context, page, }) => { await page.addInitScript(() => { localStorage.setItem("@paseo:app-settings", JSON.stringify({ uiFontFamily: "serif" })); }); const agent = await seedMockAgentWorkspace({ repoPrefix: "assistant-selection-copy-", title: "Assistant selection copy", initialPrompt: "Render the clipboard fixture.", featureValues: { mockAssistantResponse: ASSISTANT_MARKDOWN }, }); try { await allowRichClipboard(context); await agent.client.waitForAgentUpsert( agent.agentId, (snapshot) => snapshot.status === "idle", 30_000, ); await openAgentRoute(page, agent); const assistantMessage = page.getByTestId("assistant-message").filter({ hasText: "Direct matches:", }); for (const [tag, text] of [ ["strong", "strong prose"], ["em", "emphasized prose"], ["s", "struck prose"], ]) { const formattedProse = assistantMessage .locator(`[data-paseo-markdown-tag="${tag}"]`) .filter({ hasText: text }); await expect(formattedProse).toHaveCSS("font-family", "serif"); await expect(formattedProse).not.toHaveAttribute("data-pmono"); } const inlineCode = assistantMessage .locator('[data-paseo-markdown-tag="code"]') .filter({ hasText: "apply_patch" }); await expect(inlineCode).toHaveAttribute("data-pmono", ""); await selectAssistantMessage(page); await copySelection(page); const clipboard = await readRichClipboard(page); expect(clipboard.plainText).toBe(EXPECTED_WHOLE_SELECTION_MARKDOWN); expect(clipboard.html).toContain("
    "); expect(clipboard.html).toContain( 'First issue', ); expect(clipboard.html).toContain("apply_patch"); expect(clipboard.html).toContain('
      '); expect(clipboard.html).toContain("A hard break
      "); expect(clipboard.html).toContain( 'www.example.com stays plain Markdown text.', ); expect(clipboard.html).toContain( 'docs', ); expect(clipboard.html).toContain( 'https://autolink.example.com', ); expect(clipboard.html).toContain( 'workspace file', ); expect(clipboard.html).toContain("https://example.com/generated"); expect(clipboard.html).not.toContain( 'https://example.com/generated', ); expect(clipboard.html).toContain(''); expect(clipboard.html).toContain( '
      before\n```\nafter\n
      ', ); expect(clipboard.html).toContain(""); expect(clipboard.html).toContain(''); expect(clipboard.html.match(/obsolete"); expect(clipboard.html).toContain(''); expect(clipboard.html).toContain(''); expect(clipboard.html).toContain(''); await selectAssistantText(page, "First"); await copySelection(page); const partialClipboard = await readRichClipboard(page); expect(partialClipboard.plainText).toBe("- **[First](https://example.com/issues/1)**"); expect(partialClipboard.html).toContain( '
    1. First
    2. ', ); await selectAssistantText(page, "docs"); await copySelection(page); const titledLinkClipboard = await readRichClipboard(page); expect(titledLinkClipboard.plainText).toBe('[docs](https://docs.example.com "Reference")'); expect(titledLinkClipboard.html).toContain( 'docs', ); await selectAssistantText(page, "https://autolink.example.com"); await copySelection(page); const autolinkClipboard = await readRichClipboard(page); expect(autolinkClipboard.plainText).toBe( "[https://autolink.example.com](https://autolink.example.com)", ); expect(autolinkClipboard.html).toContain( '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', ); await selectAssistantTextRange(page, "Seventh item", "Eighth item"); await copySelection(page); const midListClipboard = await readRichClipboard(page); expect(midListClipboard.plainText).toBe("7. Seventh item\n8. Eighth item"); expect(midListClipboard.html).toContain('
        '); await selectAssistantTextRange(page, "Inner eight", "Inner nine"); await copySelection(page); const nestedListClipboard = await readRichClipboard(page); expect(nestedListClipboard.plainText).toBe("3. 8. Inner eight\n 9. Inner nine"); expect(nestedListClipboard.html).toContain('
          '); await selectAssistantTextRange(page, "Seventh item", "Nested list:"); await copySelection(page); const crossBlockClipboard = await readRichClipboard(page); expect(crossBlockClipboard.plainText).toBe("7. Seventh item\n8. Eighth item\n\nNested list:"); expect(crossBlockClipboard.html).toContain('
            '); await selectAssistantText(page, "Current"); await copySelection(page); const partialTableClipboard = await readRichClipboard(page); expect(partialTableClipboard.plainText).toBe("Current"); expect(partialTableClipboard.html).toContain("

            Current

            "); expect(partialTableClipboard.html).not.toContain("<table"); await selectAssistantTextRange(page, "Right", "ready"); await copySelection(page); const columnSliceClipboard = await readRichClipboard(page); expect(columnSliceClipboard.plainText).toContain("ready"); expect(columnSliceClipboard.html).not.toContain("
      Current | activeLeftRightCenter
      "); expect(columnSliceClipboard.html).toContain("ready"); expect(columnSliceClipboard.html).toContain("obsolete"); } finally { await agent.cleanup(); } });