diff --git a/src/components/Messages/AssistantMessage.tsx b/src/components/Messages/AssistantMessage.tsx index 864fe89ae..f8d0836bc 100644 --- a/src/components/Messages/AssistantMessage.tsx +++ b/src/components/Messages/AssistantMessage.tsx @@ -5,6 +5,7 @@ import { TypewriterMarkdown } from "./TypewriterMarkdown"; import type { ButtonConfig } from "./MessageWindow"; import { MessageWindow } from "./MessageWindow"; import { useStartHere } from "@/hooks/useStartHere"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { COMPACTED_EMOJI } from "@/constants/ui"; import { ModelDisplay } from "./ModelDisplay"; import { CompactingMessageContent } from "./CompactingMessageContent"; @@ -27,7 +28,6 @@ export const AssistantMessage: React.FC = ({ clipboardWriteText = (data: string) => navigator.clipboard.writeText(data), }) => { const [showRaw, setShowRaw] = useState(false); - const [copied, setCopied] = useState(false); const content = message.content; const isStreaming = message.isStreaming; @@ -42,15 +42,8 @@ export const AssistantMessage: React.FC = ({ modal, } = useStartHere(workspaceId, content, isCompacted); - const handleCopy = async () => { - try { - await clipboardWriteText(content); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error("Failed to copy:", err); - } - }; + // Copy to clipboard with feedback + const { copied, copyToClipboard } = useCopyToClipboard(clipboardWriteText); // Keep only Copy button visible (most common action) // Kebab menu saves horizontal space by collapsing less-used actions into a single ⋮ button @@ -59,7 +52,7 @@ export const AssistantMessage: React.FC = ({ : [ { label: copied ? "✓ Copied" : "Copy", - onClick: () => void handleCopy(), + onClick: () => void copyToClipboard(content), }, ]; diff --git a/src/components/Messages/UserMessage.tsx b/src/components/Messages/UserMessage.tsx index 75968b351..54edf39f6 100644 --- a/src/components/Messages/UserMessage.tsx +++ b/src/components/Messages/UserMessage.tsx @@ -1,9 +1,10 @@ -import React, { useState } from "react"; +import React from "react"; import type { DisplayedMessage } from "@/types/message"; import type { ButtonConfig } from "./MessageWindow"; import { MessageWindow } from "./MessageWindow"; import { TerminalOutput } from "./TerminalOutput"; import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import type { KebabMenuItem } from "@/components/KebabMenu"; interface UserMessageProps { @@ -30,8 +31,6 @@ export const UserMessage: React.FC = ({ isCompacting, clipboardWriteText = defaultClipboardWriteText, }) => { - const [copied, setCopied] = useState(false); - const content = message.content; console.assert( @@ -48,20 +47,8 @@ export const UserMessage: React.FC = ({ ? content.slice("".length, -"".length).trim() : ""; - const handleCopy = async () => { - console.assert( - typeof content === "string", - "UserMessage copy handler expects message content to be a string." - ); - - try { - await clipboardWriteText(content); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error("Failed to copy:", err); - } - }; + // Copy to clipboard with feedback + const { copied, copyToClipboard } = useCopyToClipboard(clipboardWriteText); const handleEdit = () => { if (onEdit && !isLocalCommandOutput) { @@ -86,7 +73,7 @@ export const UserMessage: React.FC = ({ : []), { label: copied ? "✓ Copied" : "Copy", - onClick: () => void handleCopy(), + onClick: () => void copyToClipboard(content), }, ]; diff --git a/src/components/tools/FileEditToolCall.tsx b/src/components/tools/FileEditToolCall.tsx index 75011a767..cfb3d0034 100644 --- a/src/components/tools/FileEditToolCall.tsx +++ b/src/components/tools/FileEditToolCall.tsx @@ -19,6 +19,7 @@ import { LoadingDots, } from "./shared/ToolPrimitives"; import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/toolUtils"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { TooltipWrapper, Tooltip } from "../Tooltip"; import { DiffContainer, DiffRenderer, SelectableDiffRenderer } from "../shared/DiffRenderer"; import { KebabMenu, type KebabMenuItem } from "../KebabMenu"; @@ -104,21 +105,11 @@ export const FileEditToolCall: React.FC = ({ const { expanded, toggleExpanded } = useToolExpansion(initialExpanded); const [showRaw, setShowRaw] = React.useState(false); - const [copied, setCopied] = React.useState(false); const filePath = "file_path" in args ? args.file_path : undefined; - const handleCopyPatch = async () => { - if (result && result.success && result.diff) { - try { - await navigator.clipboard.writeText(result.diff); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error("Failed to copy:", err); - } - } - }; + // Copy to clipboard with feedback + const { copied, copyToClipboard } = useCopyToClipboard(); // Build kebab menu items for successful edits with diffs const kebabMenuItems: KebabMenuItem[] = @@ -126,7 +117,7 @@ export const FileEditToolCall: React.FC = ({ ? [ { label: copied ? "✓ Copied" : "Copy Patch", - onClick: () => void handleCopyPatch(), + onClick: () => void copyToClipboard(result.diff), }, { label: showRaw ? "Show Parsed" : "Show Patch", diff --git a/src/components/tools/ProposePlanToolCall.tsx b/src/components/tools/ProposePlanToolCall.tsx index 90b08ecfd..715d5198a 100644 --- a/src/components/tools/ProposePlanToolCall.tsx +++ b/src/components/tools/ProposePlanToolCall.tsx @@ -12,6 +12,7 @@ import { useToolExpansion, getStatusDisplay, type ToolStatus } from "./shared/to import { MarkdownRenderer } from "../Messages/MarkdownRenderer"; import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds"; import { useStartHere } from "@/hooks/useStartHere"; +import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { TooltipWrapper, Tooltip } from "../Tooltip"; import { cn } from "@/lib/utils"; @@ -30,7 +31,6 @@ export const ProposePlanToolCall: React.FC = ({ }) => { const { expanded, toggleExpanded } = useToolExpansion(true); // Expand by default const [showRaw, setShowRaw] = useState(false); - const [copied, setCopied] = useState(false); // Format: Title as H1 + plan content for "Start Here" functionality const startHereContent = `# ${args.title}\n\n${args.plan}`; @@ -46,20 +46,13 @@ export const ProposePlanToolCall: React.FC = ({ false // Plans are never already compacted ); + // Copy to clipboard with feedback + const { copied, copyToClipboard } = useCopyToClipboard(); + const [isHovered, setIsHovered] = useState(false); const statusDisplay = getStatusDisplay(status); - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(args.plan); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - console.error("Failed to copy:", err); - } - }; - return ( @@ -134,7 +127,7 @@ export const ProposePlanToolCall: React.FC = ({ )}