Repository navigation
feat: AI output copy to clipboard - #368
Conversation
|
Cursor Agent can help with this pull request. Just |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📝 WalkthroughWalkthroughAdds 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
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/ai-core/src/components/AnalysisAnswer.tsx (1)
135-135:hasMarkdownContentis a misnomer — it checks for any non-empty content, not markdown specifically.Minor naming nit:
hasContentorhasTextContentwould 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.
There was a problem hiding this comment.
🤖 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]>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/ai-core/src/components/AnalysisAnswer.tsx (2)
26-26:THINK_TAGS_REGEXis a near-duplicate ofCOMPLETE_THINK_REGEX.Both patterns are
/<think>[\s\S]*?<\/think>/g;COMPLETE_THINK_REGEXjust adds a capture group that is irrelevant when replacing with''. ReusingCOMPLETE_THINK_REGEXeliminates 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:hasTextContentmay show the CopyButton when copyable content would be empty.
content.trim().length > 0passes 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.
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
packages/ai-core/src/components/AnalysisResult.tsx (1)
95-104:group/promptclass appears unused after simplification.The
group/promptnamed group class was likely used to drive the oldgroup-hover/prompt:opacity-100on 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: Hardcodedh-8 w-8can conflict with thesizeprop.The
cn(…)call always includesh-8 w-8, which will override the dimensions set by thesizevariant (e.g.,size="xs"orsize="sm"). InQueryToolResult.tsx, the caller passesclassName="h-6 w-6"to work around this, but the ordering incnmeans the hardcoded classes may still win depending on the CSS layer.Consider removing the hardcoded dimensions and letting the
sizeprop 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:isSuccessandcontentprops are accepted but unused.Both
isSuccessandcontentare defined inMessageContainerPropsand destructured (or inisSuccess'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).
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
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.
Summary by CodeRabbit
New Features
Improvements