From 7dc599bff86fba811bea0716d4021f7503900d24 Mon Sep 17 00:00:00 2001 From: Illia Panasenko Date: Wed, 29 Apr 2026 13:49:24 +0200 Subject: [PATCH 1/3] Add copy path button to diff headers --- .../src/components/DiffFilePathCopyButton.tsx | 74 +++++++++++++++++++ apps/web/src/components/DiffPanel.tsx | 4 + 2 files changed, 78 insertions(+) create mode 100644 apps/web/src/components/DiffFilePathCopyButton.tsx diff --git a/apps/web/src/components/DiffFilePathCopyButton.tsx b/apps/web/src/components/DiffFilePathCopyButton.tsx new file mode 100644 index 0000000000..bfc749e831 --- /dev/null +++ b/apps/web/src/components/DiffFilePathCopyButton.tsx @@ -0,0 +1,74 @@ +import { CheckIcon, CopyIcon } from "lucide-react"; +import { type MouseEvent as ReactMouseEvent, type RefObject, useRef } from "react"; +import { useCopyToClipboard } from "../hooks/useCopyToClipboard"; +import { Button } from "./ui/button"; +import { anchoredToastManager } from "./ui/toast"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; + +const ANCHORED_TOAST_TIMEOUT_MS = 1000; + +function showCopySuccessToast(ref: RefObject) { + if (!ref.current) return; + anchoredToastManager.add({ + data: { + tooltipStyle: true, + }, + positionerProps: { + anchor: ref.current, + }, + timeout: ANCHORED_TOAST_TIMEOUT_MS, + title: "Copied!", + }); +} + +function showCopyErrorToast(ref: RefObject, error: Error) { + if (!ref.current) return; + anchoredToastManager.add({ + data: { + tooltipStyle: true, + }, + positionerProps: { + anchor: ref.current, + }, + timeout: ANCHORED_TOAST_TIMEOUT_MS, + title: "Failed to copy", + description: error.message, + }); +} + +export function DiffFilePathCopyButton({ filePath }: { filePath: string }) { + const ref = useRef(null); + const { copyToClipboard, isCopied } = useCopyToClipboard({ + onCopy: () => showCopySuccessToast(ref), + onError: (error) => showCopyErrorToast(ref, error), + timeout: ANCHORED_TOAST_TIMEOUT_MS, + }); + + const onClick = (event: ReactMouseEvent) => { + event.stopPropagation(); + copyToClipboard(filePath, undefined); + }; + + return ( + + + } + > + {isCopied ? : } + + +

{isCopied ? "Copied" : "Copy path"}

+
+
+ ); +} diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index e6dbb57cc7..7f6fcb361d 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -35,6 +35,7 @@ import { createThreadSelectorByRef } from "../storeSelectors"; import { buildThreadRouteParams, resolveThreadRouteRef } from "../threadRoutes"; import { useSettings } from "../hooks/useSettings"; import { formatShortTimestamp } from "../timestampFormat"; +import { DiffFilePathCopyButton } from "./DiffFilePathCopyButton"; import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; import { ToggleGroup, Toggle } from "./ui/toggle-group"; @@ -622,6 +623,9 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { > + filePath ? : null + } options={{ diffStyle: diffRenderMode === "split" ? "split" : "unified", lineDiffType: "none", From 558bb1772ed641cd9f1f7f19c9d6e84afcc30596 Mon Sep 17 00:00:00 2001 From: Illia Panasenko Date: Wed, 29 Apr 2026 14:01:05 +0200 Subject: [PATCH 2/3] remove condition --- apps/web/src/components/DiffPanel.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 7f6fcb361d..5b74a42005 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -623,9 +623,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { > - filePath ? : null - } + renderHeaderMetadata={() => } options={{ diffStyle: diffRenderMode === "split" ? "split" : "unified", lineDiffType: "none", From ff25a41773a8598c202549ba80feb61bec634490 Mon Sep 17 00:00:00 2001 From: Illia Panasenko Date: Wed, 29 Apr 2026 14:13:34 +0200 Subject: [PATCH 3/3] address comments from cursorbot + inline onClick --- .../src/components/DiffFilePathCopyButton.tsx | 53 ++++--------------- .../src/components/chat/MessageCopyButton.tsx | 44 +++------------ .../src/components/ui/anchoredCopyToast.ts | 33 ++++++++++++ 3 files changed, 51 insertions(+), 79 deletions(-) create mode 100644 apps/web/src/components/ui/anchoredCopyToast.ts diff --git a/apps/web/src/components/DiffFilePathCopyButton.tsx b/apps/web/src/components/DiffFilePathCopyButton.tsx index bfc749e831..0c8a273e61 100644 --- a/apps/web/src/components/DiffFilePathCopyButton.tsx +++ b/apps/web/src/components/DiffFilePathCopyButton.tsx @@ -1,54 +1,22 @@ import { CheckIcon, CopyIcon } from "lucide-react"; -import { type MouseEvent as ReactMouseEvent, type RefObject, useRef } from "react"; +import { useRef } from "react"; import { useCopyToClipboard } from "../hooks/useCopyToClipboard"; import { Button } from "./ui/button"; -import { anchoredToastManager } from "./ui/toast"; +import { + ANCHORED_COPY_TOAST_TIMEOUT_MS, + showAnchoredCopyErrorToast, + showAnchoredCopySuccessToast, +} from "./ui/anchoredCopyToast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; -const ANCHORED_TOAST_TIMEOUT_MS = 1000; - -function showCopySuccessToast(ref: RefObject) { - if (!ref.current) return; - anchoredToastManager.add({ - data: { - tooltipStyle: true, - }, - positionerProps: { - anchor: ref.current, - }, - timeout: ANCHORED_TOAST_TIMEOUT_MS, - title: "Copied!", - }); -} - -function showCopyErrorToast(ref: RefObject, error: Error) { - if (!ref.current) return; - anchoredToastManager.add({ - data: { - tooltipStyle: true, - }, - positionerProps: { - anchor: ref.current, - }, - timeout: ANCHORED_TOAST_TIMEOUT_MS, - title: "Failed to copy", - description: error.message, - }); -} - export function DiffFilePathCopyButton({ filePath }: { filePath: string }) { const ref = useRef(null); const { copyToClipboard, isCopied } = useCopyToClipboard({ - onCopy: () => showCopySuccessToast(ref), - onError: (error) => showCopyErrorToast(ref, error), - timeout: ANCHORED_TOAST_TIMEOUT_MS, + onCopy: () => showAnchoredCopySuccessToast(ref), + onError: (error) => showAnchoredCopyErrorToast(ref, error), + timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, }); - const onClick = (event: ReactMouseEvent) => { - event.stopPropagation(); - copyToClipboard(filePath, undefined); - }; - return ( copyToClipboard(filePath, undefined)} /> } > diff --git a/apps/web/src/components/chat/MessageCopyButton.tsx b/apps/web/src/components/chat/MessageCopyButton.tsx index ad5d56dd5a..9de2c757bc 100644 --- a/apps/web/src/components/chat/MessageCopyButton.tsx +++ b/apps/web/src/components/chat/MessageCopyButton.tsx @@ -3,41 +3,13 @@ import { CopyIcon, CheckIcon } from "lucide-react"; import { Button } from "../ui/button"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { cn } from "~/lib/utils"; -import { anchoredToastManager } from "../ui/toast"; +import { + ANCHORED_COPY_TOAST_TIMEOUT_MS, + showAnchoredCopyErrorToast, + showAnchoredCopySuccessToast, +} from "../ui/anchoredCopyToast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; -const ANCHORED_TOAST_TIMEOUT_MS = 1000; -const onCopy = (ref: React.RefObject) => { - if (ref.current) { - anchoredToastManager.add({ - data: { - tooltipStyle: true, - }, - positionerProps: { - anchor: ref.current, - }, - timeout: ANCHORED_TOAST_TIMEOUT_MS, - title: "Copied!", - }); - } -}; - -const onCopyError = (ref: React.RefObject, error: Error) => { - if (ref.current) { - anchoredToastManager.add({ - data: { - tooltipStyle: true, - }, - positionerProps: { - anchor: ref.current, - }, - timeout: ANCHORED_TOAST_TIMEOUT_MS, - title: "Failed to copy", - description: error.message, - }); - } -}; - export const MessageCopyButton = memo(function MessageCopyButton({ text, size = "xs", @@ -51,9 +23,9 @@ export const MessageCopyButton = memo(function MessageCopyButton({ }) { const ref = useRef(null); const { copyToClipboard, isCopied } = useCopyToClipboard({ - onCopy: () => onCopy(ref), - onError: (error: Error) => onCopyError(ref, error), - timeout: ANCHORED_TOAST_TIMEOUT_MS, + onCopy: () => showAnchoredCopySuccessToast(ref), + onError: (error: Error) => showAnchoredCopyErrorToast(ref, error), + timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, }); return ( diff --git a/apps/web/src/components/ui/anchoredCopyToast.ts b/apps/web/src/components/ui/anchoredCopyToast.ts new file mode 100644 index 0000000000..df1ac579c8 --- /dev/null +++ b/apps/web/src/components/ui/anchoredCopyToast.ts @@ -0,0 +1,33 @@ +import type { RefObject } from "react"; +import { anchoredToastManager } from "./toast"; + +export const ANCHORED_COPY_TOAST_TIMEOUT_MS = 1000; + +export function showAnchoredCopySuccessToast(ref: RefObject) { + if (!ref.current) return; + anchoredToastManager.add({ + data: { + tooltipStyle: true, + }, + positionerProps: { + anchor: ref.current, + }, + timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, + title: "Copied!", + }); +} + +export function showAnchoredCopyErrorToast(ref: RefObject, error: Error) { + if (!ref.current) return; + anchoredToastManager.add({ + data: { + tooltipStyle: true, + }, + positionerProps: { + anchor: ref.current, + }, + timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, + title: "Failed to copy", + description: error.message, + }); +}