feat: copy button on user messages, hover on message for buttons - #110
Conversation
WalkthroughA MessageCopyButton component was added to ChatView.tsx that copies message text to the clipboard and displays brief visual feedback. The button is integrated into the MessagesTimeline component, appearing alongside existing message actions with hover activation and layout adjustments for consistent spacing. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/src/components/ChatView.tsx`:
- Around line 2341-2344: handleCopy currently calls
navigator.clipboard.writeText without handling rejections and always sets
setCopied(true), and the timeout is never cleared on unmount; update handleCopy
to await or use .then/.catch on navigator.clipboard.writeText(text) and only
call setCopied(true) when the promise resolves, log or surface the error on
rejection and avoid setting copied on failure, store the timeout id in a ref
(e.g., copyTimeoutRef) and use clearTimeout(copyTimeoutRef.current) before
setting a new timeout, and add a cleanup (in a useEffect return) to clear the
timeout on unmount; reference handleCopy, navigator.clipboard.writeText,
setCopied and the timeout logic when making changes.
| const handleCopy = useCallback(() => { | ||
| void navigator.clipboard.writeText(text); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); |
There was a problem hiding this comment.
Handle clipboard API errors and clean up timeout on unmount.
navigator.clipboard.writeText can reject (permission denied, insecure context). Currently, failures are silently ignored and the user sees "copied" feedback regardless. Additionally, the timeout should be cleaned up if the component unmounts.
🛡️ Proposed fix
const MessageCopyButton = memo(function MessageCopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(() => {
- void navigator.clipboard.writeText(text);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
+ navigator.clipboard.writeText(text).then(
+ () => setCopied(true),
+ () => {
+ // Optionally show error feedback or silently fail
+ }
+ );
}, [text]);
+
+ useEffect(() => {
+ if (!copied) return;
+ const timer = setTimeout(() => setCopied(false), 2000);
+ return () => clearTimeout(timer);
+ }, [copied]);
return (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/ChatView.tsx` around lines 2341 - 2344, handleCopy
currently calls navigator.clipboard.writeText without handling rejections and
always sets setCopied(true), and the timeout is never cleared on unmount; update
handleCopy to await or use .then/.catch on navigator.clipboard.writeText(text)
and only call setCopied(true) when the promise resolves, log or surface the
error on rejection and avoid setting copied on failure, store the timeout id in
a ref (e.g., copyTimeoutRef) and use clearTimeout(copyTimeoutRef.current) before
setting a new timeout, and add a cleanup (in a useEffect return) to clear the
timeout on unmount; reference handleCopy, navigator.clipboard.writeText,
setCopied and the timeout logic when making changes.
Merges upstream t3code into the fork, from the merge base `36c4e9cf` through upstream `b438447f`: 395 non-merge upstream commits, 1175 files changed upstream against 658 in the fork. Stacks on `harden-fork-merge` (pingdotgg#106), whose merge tooling this merge exercises end to end. ## Conflict resolution, by concern 17 files conflicted. Resolved by concern: - **Contracts** (`packages/contracts/src/rpc.ts`, converged): kept the fork's import block (sandbox, servers, subtasks, threadShellLookup, scripts) alongside upstream's new `providerUsageLimits` import, and re-applied the fork's `UnsupportedMethodError` union entries onto the RPC declarations upstream reformatted. - **Chat and shell**: `ChatMarkdown` keeps the mermaid fence branch on upstream's new module-level renderer; `Sidebar` keeps the `FEATURES` and `useTouchContextMenu` imports; `MessagesTimeline` keeps the fork's thread-forking activity state and `AssistantForkButton`; `SidebarChrome` keeps the `APP_BASE_NAME` span in place of upstream's `T3Wordmark`. - **Preview**: `previewStateStore` keeps `previewRuntimeCapability` and `isPreviewSupportedInRuntime`; `PreviewView` and `ThreadPreviewMiniPlayer` take upstream's z-index work and keep the fork's framed-preview surface and `hasPreviewSurface` gating. - **Settings**: upstream's six-section split, with the fork's `assistantStreaming`, `projectGrouping`, `projectManagement` and `threadDeletion` gates preserved inside it, plus the personal/admin nav split. - **Docs, AGENTS.md, CI**: `ci.yml` converges upstream's apt-mirror and libsecret steps with the fork's Moatless API spec-freshness step; `docs/README.md` is the union of both indexes with dead links dropped. `apps/web/src/AppRoot.test.tsx` was a modify/delete: kept, because it still guards the fork-only `WebBrowserHost` render tree. `pnpm-lock.yaml` took upstream per path policy. Verification and any follow-ups are recorded in `docs/fork/upstream-merge-log.md`. --- Moatless task: https://moatless.soaplabstest.com/tasks/5f0f1eaf-ab9d-4410-bcc4-f79b4c1f3cef
Note
Add a copy button to user message bubbles and show message controls on hover in
apps/web/src/components/ChatView.tsxIntroduce a memoized
MessageCopyButtonthat writes text to the clipboard and togglesCopyIcon/CheckIconfor 2000ms, and update user message bubbles to reveal copy and revert controls on hover/focus with opacity transitions in ChatView.tsx.📍Where to Start
Start with the
MessageCopyButtoncomponent and its usage withinMessagesTimelinein ChatView.tsx.Macroscope summarized e09f859.
Summary by CodeRabbit