Skip to content

feat: AI output copy to clipboard - #368

Merged
ilyabo merged 8 commits into
mainfrom
cursor/ai-output-copy-to-clipboard-d119
Feb 18, 2026
Merged

ilyabo merged 8 commits into
mainfrom
cursor/ai-output-copy-to-clipboard-d119

Conversation

@ilyabo

@ilyabo ilyabo commented Feb 17, 2026 •

Copy link
Copy Markdown
Collaborator

Add "copy to clipboard" functionality for AI assistant outputs as Markdown.

This improves user experience by allowing users to easily copy AI responses in their original Markdown format, mirroring the existing prompt copy behavior.

image

Open in Cursor Open in Web

Summary by CodeRabbit

  • New Features

    • Copy-to-clipboard added to AI responses and SQL/query snippets with contextual tooltip labels.
    • Message layout supports optional footer actions so copy controls appear consistently in message footers.
    • Copy can capture dynamically computed text at click time.
  • Improvements

    • Copy controls reveal on hover/focus and show clearer success state and tooltip behavior for better accessibility.

@cursor

cursor Bot commented Feb 17, 2026

Copy link
Copy Markdown

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@coderabbitai

coderabbitai Bot commented Feb 17, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@ilyabo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 52 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds a clipboard copy UI flow: CopyButton now accepts dynamic text and tooltipLabel, MessageContainer gains footerActions/footerActionsClassName, and AnalysisAnswer/AnalysisResult/QueryToolResult integrate sanitized copy actions into MessageContainer footers.

Changes

Cohort / File(s) Summary
Copy Button Enhancement
packages/ui/src/components/copy-button.tsx
Widened text prop to `string
MessageContainer Footer Actions
packages/ai-core/src/components/MessageContainer.tsx
Added footerActions?: React.ReactNode and footerActionsClassName?: string; renders an optional footer action block with alignment/transition classes; adjusted badge positioning class ordering.
AnalysisAnswer Integration
packages/ai-core/src/components/AnalysisAnswer.tsx
Introduced sanitizeThinkTags to derive copyableText and hasTextContent; constructs footerActions (CopyButton with contextual tooltipLabel) and passes it into MessageContainer, removing an outer wrapper.
AnalysisResult Copy UX
packages/ai-core/src/components/AnalysisResult.tsx
Adjusted prompt group styling to show copy actions on hover/focus-within and replaced explicit CopyButton styling/ariaLabel with tooltipLabel.
QueryToolResult Copy Update
packages/ai/src/tools/query/QueryToolResult.tsx
Updated CopyButton usage: removed explicit variant/size/ariaLabel, added tooltipLabel="Copy SQL query", minor import reformatting, and applied group hover/focus styles.
Manifest / package metadata
package.json
Small manifest line changes recorded by analyzer.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MessageContainer
  participant CopyButton
  participant Clipboard

  User->>MessageContainer: view message (with footerActions)
  MessageContainer->>CopyButton: render footer action (tooltipLabel, text or text() prop)
  User->>CopyButton: click copy
  CopyButton->>CopyButton: resolve textToCopy (call function if provided)
  CopyButton->>Clipboard: navigator.clipboard.writeText(textToCopy)
  Clipboard-->>CopyButton: success
  CopyButton-->>User: show copied state (icon change + tooltip)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I nibble tags that cloud the view,
I pull the text and make it true,
A little click, the clipboard sings,
Footer buttons flap their wings,
Copy done — the rabbit grins! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature being added: copy-to-clipboard functionality for AI outputs.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor/ai-output-copy-to-clipboard-d119

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ilyabo ilyabo changed the title AI output copy to clipboard feat: AI output copy to clipboard Feb 17, 2026
@ilyabo
ilyabo marked this pull request as ready for review February 17, 2026 20:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/ai-core/src/components/AnalysisAnswer.tsx (1)

135-135: hasMarkdownContent is a misnomer — it checks for any non-empty content, not markdown specifically.

Minor naming nit: hasContent or hasTextContent would be more accurate. Currently any whitespace-only string is excluded, which is fine, but the name suggests markdown detection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-core/src/components/AnalysisAnswer.tsx` at line 135, The variable
name hasMarkdownContent is misleading because it only checks for non-empty
trimmed content; rename it to hasTextContent (or hasContent) in
AnalysisAnswer.tsx and update all references (e.g., where hasMarkdownContent is
used) to the new identifier, preserving the same trim() check and boolean logic
so behaviour remains identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ai-core/src/components/AnalysisAnswer.tsx`:
- Around line 154-166: The CopyButton currently receives raw content which may
include <think>…</think> blocks, causing those tags to be copied; update the
footerActions construction so that before passing content to CopyButton you
strip out any <think>...</think> blocks (e.g., with a small helper like
removeThinkTags or sanitizeThinkTags) and pass the cleaned string instead (keep
using hasMarkdownContent and isAnswer as before); ensure the helper is
referenced where footerActions is defined so CopyButton.text gets the sanitized
value.

---

Nitpick comments:
In `@packages/ai-core/src/components/AnalysisAnswer.tsx`:
- Line 135: The variable name hasMarkdownContent is misleading because it only
checks for non-empty trimmed content; rename it to hasTextContent (or
hasContent) in AnalysisAnswer.tsx and update all references (e.g., where
hasMarkdownContent is used) to the new identifier, preserving the same trim()
check and boolean logic so behaviour remains identical.

Comment thread packages/ai-core/src/components/AnalysisAnswer.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/ai-core/src/components/AnalysisAnswer.tsx`:
- Around line 154-166: The CopyButton is receiving raw content that still
contains <think>...</think> blocks (see footerActions and CopyButton usage);
before passing content to the CopyButton, create and use a small sanitizer
(e.g., stripThinkTags or sanitizeForCopy) that removes all <think>...</think>
segments (use a non-greedy, dot-all regex or DOM parsing to strip tags and inner
text) and pass that sanitized string into the CopyButton's text prop while
leaving the original content unchanged for rendering; update the footerActions
to use the sanitized value when calling CopyButton (keep isAnswer and
hasMarkdownContent logic intact).

Signed-off-by: Ilya Boyandin <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/ai-core/src/components/AnalysisAnswer.tsx (2)

26-26: THINK_TAGS_REGEX is a near-duplicate of COMPLETE_THINK_REGEX.

Both patterns are /<think>[\s\S]*?<\/think>/g; COMPLETE_THINK_REGEX just adds a capture group that is irrelevant when replacing with ''. Reusing COMPLETE_THINK_REGEX eliminates the duplication:

♻️ Proposed fix
-const THINK_TAGS_REGEX = /<think>[\s\S]*?<\/think>/g;
-
-const sanitizeThinkTags = (value: string): string =>
-  value.replace(THINK_TAGS_REGEX, '');
+const sanitizeThinkTags = (value: string): string =>
+  value.replace(COMPLETE_THINK_REGEX, '').replace(INCOMPLETE_THINK_REGEX, '').trim();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-core/src/components/AnalysisAnswer.tsx` at line 26,
THINK_TAGS_REGEX duplicates COMPLETE_THINK_REGEX; remove THINK_TAGS_REGEX and
replace its usages with COMPLETE_THINK_REGEX to eliminate redundancy. Locate the
constant THINK_TAGS_REGEX and any references to it in AnalysisAnswer.tsx, delete
the THINK_TAGS_REGEX declaration, and update call sites to use
COMPLETE_THINK_REGEX instead (preserving the global flag and replacement
behavior). Ensure imports or exports are adjusted if COMPLETE_THINK_REGEX is
defined elsewhere so the symbol is available in AnalysisAnswer.tsx.

139-139: hasTextContent may show the CopyButton when copyable content would be empty.

content.trim().length > 0 passes if the raw string contains only <think>…</think> blocks. After sanitization those are stripped, so clicking copy would write an empty string. Consider guarding against this:

♻️ Proposed fix
-  const hasTextContent = content.trim().length > 0;
+  const hasTextContent = useMemo(
+    () => sanitizeThinkTags(content).trim().length > 0,
+    [content],
+  );

Also applies to: 158-170

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-core/src/components/AnalysisAnswer.tsx` at line 139, The current
hasTextContent uses the raw content string (const hasTextContent =
content.trim().length > 0;) which can be true even when sanitized output is
empty (e.g., content only contains <think>…</think>); change the check to
compute the sanitized/visible copy text first (reuse whatever
sanitization/strip-think logic used for rendering in AnalysisAnswer) and base
hasTextContent on that sanitized text length; update both occurrences (around
hasTextContent at the current line and the similar block at lines ~158-170) so
the CopyButton and any copy action use the same sanitized text variable (e.g.,
copyableText or sanitizedContent) to decide visibility and to perform copying.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ai-core/src/components/MessageContainer.tsx`:
- Around line 61-70: The footer actions are hidden with only mouse hover styles;
update the class string where footerActions is rendered in MessageContainer.tsx
(the cn call using footerActionsClassName) to also include
"group-focus-within:opacity-100" so the footer becomes visible when any child
(e.g., CopyButton) receives keyboard focus; ensure the parent element retains
the "group" utility so focus-within will trigger visibility for keyboard users.

---

Duplicate comments:
In `@packages/ai-core/src/components/AnalysisAnswer.tsx`:
- Around line 26-29: sanitizeThinkTags currently only removes complete
<think>...</think> pairs via THINK_TAGS_REGEX, allowing stray open tags and
partial content from streaming to remain; update the sanitizeThinkTags function
to run two global replacements: first remove matches of THINK_TAGS_REGEX (or
COMPLETE_THINK_REGEX if present) and then remove matches of
INCOMPLETE_THINK_REGEX so any lone opening <think> tags and following partial
text are stripped; ensure both regexes are applied
(value.replace(...).replace(...)) and return the cleaned string.

---

Nitpick comments:
In `@packages/ai-core/src/components/AnalysisAnswer.tsx`:
- Line 26: THINK_TAGS_REGEX duplicates COMPLETE_THINK_REGEX; remove
THINK_TAGS_REGEX and replace its usages with COMPLETE_THINK_REGEX to eliminate
redundancy. Locate the constant THINK_TAGS_REGEX and any references to it in
AnalysisAnswer.tsx, delete the THINK_TAGS_REGEX declaration, and update call
sites to use COMPLETE_THINK_REGEX instead (preserving the global flag and
replacement behavior). Ensure imports or exports are adjusted if
COMPLETE_THINK_REGEX is defined elsewhere so the symbol is available in
AnalysisAnswer.tsx.
- Line 139: The current hasTextContent uses the raw content string (const
hasTextContent = content.trim().length > 0;) which can be true even when
sanitized output is empty (e.g., content only contains <think>…</think>); change
the check to compute the sanitized/visible copy text first (reuse whatever
sanitization/strip-think logic used for rendering in AnalysisAnswer) and base
hasTextContent on that sanitized text length; update both occurrences (around
hasTextContent at the current line and the similar block at lines ~158-170) so
the CopyButton and any copy action use the same sanitized text variable (e.g.,
copyableText or sanitizedContent) to decide visibility and to perform copying.

Comment thread packages/ai-core/src/components/MessageContainer.tsx
Signed-off-by: Ilya Boyandin <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/ai-core/src/components/AnalysisResult.tsx (1)

95-104: group/prompt class appears unused after simplification.

The group/prompt named group class was likely used to drive the old group-hover/prompt:opacity-100 on the copy button area. Since the copy area is now always visible (no opacity transition), this named group selector seems unnecessary. Consider removing it to avoid confusion:

-        <div className="group/prompt bg-muted flex w-full items-center gap-2 rounded-md border p-2 text-sm">
+        <div className="bg-muted flex w-full items-center gap-2 rounded-md border p-2 text-sm">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-core/src/components/AnalysisResult.tsx` around lines 95 - 104,
The className "group/prompt" on the wrapper div is leftover from the old
hover-based visibility and is no longer used; remove the named group from the
div's className that wraps the prompt (the element rendering
analysisResult.prompt and the CopyButton) so only active utility classes remain,
ensuring the surrounding JSX (the div containing SquareTerminalIcon, the prompt
text, and the CopyButton component) still keeps its layout and styling without
the unused "group/prompt" token.
packages/ui/src/components/copy-button.tsx (1)

62-68: Hardcoded h-8 w-8 can conflict with the size prop.

The cn(…) call always includes h-8 w-8, which will override the dimensions set by the size variant (e.g., size="xs" or size="sm"). In QueryToolResult.tsx, the caller passes className="h-6 w-6" to work around this, but the ordering in cn means the hardcoded classes may still win depending on the CSS layer.

Consider removing the hardcoded dimensions and letting the size prop control the button's dimensions, or guard them with a conditional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/copy-button.tsx` around lines 62 - 68, The
Button's className currently hardcodes "h-8 w-8" inside the cn(...) call which
clashes with the size prop; update the CopyButton render to stop forcing those
dimensions — either remove "h-8 w-8" from the cn(...) value so the Button's size
variant (size prop) controls dimensions, or replace them with a conditional
mapping keyed by the size prop (e.g., map size => corresponding height/width
classes) and apply that mapping when building the className passed to Button
(affecting the Button component usage and the cn(...) call in copy-button.tsx).
packages/ai-core/src/components/MessageContainer.tsx (1)

4-13: isSuccess and content props are accepted but unused.

Both isSuccess and content are defined in MessageContainerProps and destructured (or in isSuccess's case, not even destructured), but neither is referenced in the rendered output. This is pre-existing, but since you're touching this component, it might be worth cleaning up to avoid confusion for future contributors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-core/src/components/MessageContainer.tsx` around lines 4 - 13,
MessageContainerProps declares isSuccess and content but they are unused; remove
these two props from the MessageContainerProps type and from the
MessageContainer component's props/destructuring so the component signature only
includes the actually used props (className, type, children, footerActions,
footerActionsClassName), and then update any call sites to stop passing
isSuccess/content or to pass their data via children or another explicit prop if
intended.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ui/src/components/copy-button.tsx`:
- Around line 22-32: The ariaLabel currently defaults to props.tooltipLabel
which can be undefined even though tooltipLabel has a local default; update the
destructuring so ariaLabel is computed after resolving tooltipLabel (use the
local variable tooltipLabel) and fall back to that when ariaLabel is not
provided—i.e., keep destructuring for tooltipLabel = 'Copy to clipboard' and
then set ariaLabel = props.ariaLabel ?? tooltipLabel (or similar) after to
ensure the resolved tooltipLabel is used for the accessible label in the
CopyButton component.

---

Nitpick comments:
In `@packages/ai-core/src/components/AnalysisResult.tsx`:
- Around line 95-104: The className "group/prompt" on the wrapper div is
leftover from the old hover-based visibility and is no longer used; remove the
named group from the div's className that wraps the prompt (the element
rendering analysisResult.prompt and the CopyButton) so only active utility
classes remain, ensuring the surrounding JSX (the div containing
SquareTerminalIcon, the prompt text, and the CopyButton component) still keeps
its layout and styling without the unused "group/prompt" token.

In `@packages/ai-core/src/components/MessageContainer.tsx`:
- Around line 4-13: MessageContainerProps declares isSuccess and content but
they are unused; remove these two props from the MessageContainerProps type and
from the MessageContainer component's props/destructuring so the component
signature only includes the actually used props (className, type, children,
footerActions, footerActionsClassName), and then update any call sites to stop
passing isSuccess/content or to pass their data via children or another explicit
prop if intended.

In `@packages/ui/src/components/copy-button.tsx`:
- Around line 62-68: The Button's className currently hardcodes "h-8 w-8" inside
the cn(...) call which clashes with the size prop; update the CopyButton render
to stop forcing those dimensions — either remove "h-8 w-8" from the cn(...)
value so the Button's size variant (size prop) controls dimensions, or replace
them with a conditional mapping keyed by the size prop (e.g., map size =>
corresponding height/width classes) and apply that mapping when building the
className passed to Button (affecting the Button component usage and the cn(...)
call in copy-button.tsx).

Comment thread packages/ui/src/components/copy-button.tsx Outdated
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
@ilyabo
ilyabo merged commit bcc10c1 into main Feb 18, 2026
3 checks passed
@ilyabo
ilyabo deleted the cursor/ai-output-copy-to-clipboard-d119 branch February 18, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants