feat: add copy functionality for ai responses with Dyad tag formatting (#1290) (#1315)

## Summary
Adds AI response copy functionality to chat messages that preserves
formatting and converts Dyad-specific tags to clean, readable markdown.

## Changes
- **New `useCopyToClipboard` hook**: Parses Dyad tags and converts them
to professional markdown format
- **Updated `ChatMessage` component**: Positions copy button on left
side of approval status
- **Dyad tag conversion**: Transforms custom tags to readable format:
  - `<dyad-write>` → `### File: path/to/file.js` + code block
  - `<dyad-edit>` → `### Edit: path/to/file.js` + code block  
  - `<dyad-execute-sql>` → `### Execute SQL` + ```sql block
  - `<think>` → `### Thinking` + content

## Features
-  Automatic programming language detection from file extensions  
-  Professional markdown formatting with proper headings and code
blocks
-  Tooltip showing "Copied" confirmation
-  Reuses existing DyadMarkdownParser logic for consistency

closes (#1290)
    
<!-- This is an auto-generated description by cubic. -->
---

## Summary by cubic
Adds a Copy button to assistant messages that copies a clean Markdown
version of the response by converting Dyad tags and preserving code
blocks. Improves shareability and removes Dyad-only markup; addresses
Linear #1290.

- **New Features**
- Added useCopyToClipboard hook that parses Dyad tags to Markdown,
auto-detects code language, and cleans spacing.
- Updated ChatMessage to show a Copy button (with Copy/Copied tooltip)
to the left of approval status; disabled while streaming.
- Tag conversions: think → "### Thinking"; dyad-write/edit → "###
File/Edit: path" + fenced code; dyad-execute-sql → "### Execute SQL" +
sql block; other Dyad tags map to concise headings; chat-summary/command
are omitted.
- Added e2e tests for clipboard copy, Dyad tag stripping/formatting, and
tooltip states.

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Adeniji Adekunle James
2025-09-23 07:32:55 +01:00
committed by GitHub
parent 9cca1d2af0
commit 2597d50529
5 changed files with 451 additions and 44 deletions

View File

@@ -0,0 +1,71 @@
import { test } from "./helpers/test_helper";
import { expect } from "@playwright/test";
test("copy message content - basic functionality", async ({ po }) => {
await po.setUp({ autoApprove: true });
await po.importApp("minimal");
await po.sendPrompt("[dump] Just say hello without creating any files");
await po.page
.context()
.grantPermissions(["clipboard-read", "clipboard-write"]);
const copyButton = po.page.getByTestId("copy-message-button").first();
await copyButton.click();
const clipboardContent = await po.page.evaluate(() =>
navigator.clipboard.readText(),
);
// Test that copy functionality works
expect(clipboardContent.length).toBeGreaterThan(0);
expect(clipboardContent).not.toContain("<dyad-");
});
test("copy message content - dyad-write conversion", async ({ po }) => {
await po.setUp({ autoApprove: true });
await po.importApp("minimal");
await po.sendPrompt(
"Create a simple React component in src/components/Button.tsx",
);
await po.page
.context()
.grantPermissions(["clipboard-read", "clipboard-write"]);
const copyButton = po.page.getByTestId("copy-message-button").first();
await copyButton.click();
const clipboardContent = await po.page.evaluate(() =>
navigator.clipboard.readText(),
);
// Should convert dyad-write to markdown format (flexible path matching)
expect(clipboardContent).toContain("### File:");
expect(clipboardContent).toContain("```");
expect(clipboardContent).not.toContain("<dyad-write");
});
test("copy button tooltip states", async ({ po }) => {
await po.setUp({ autoApprove: true });
await po.importApp("minimal");
await po.sendPrompt("Say hello");
const copyButton = po.page.getByTestId("copy-message-button").first();
// Check initial tooltip
await copyButton.hover();
const tooltip = po.page.locator('[role="tooltip"]');
await expect(tooltip).toHaveText("Copy");
// Copy and check "Copied!" state
await po.page
.context()
.grantPermissions(["clipboard-read", "clipboard-write"]);
await copyButton.click();
await copyButton.hover();
await expect(tooltip).toHaveText("Copied!");
});