authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-02 00:47:47-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-02 00:47:47-07:00
log60f7ae86bbeaa95ebdfeb0fd83edc564119258df
treee9c2e25d26dcc594ba2631a2d1294506bbf4b0a7
parent867cf4cf091b2626930e58d46c9fee9f009b6916
signature Signed by PGP key B5690EEEBB952194

Add copy-to-clipboard action for proposed plan card (#1620)

Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>

1 files changed, 17 insertions(+), 0 deletions(-)

apps/web/src/components/chat/ProposedPlanCard.tsx+17
...@@ -25,6 +25,7 @@ import {...@@ -25,6 +25,7 @@ import {
25} from "../ui/dialog";25} from "../ui/dialog";
26import { toastManager } from "../ui/toast";26import { toastManager } from "../ui/toast";
27import { readNativeApi } from "~/nativeApi";27import { readNativeApi } from "~/nativeApi";
28import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
2829
29export const ProposedPlanCard = memo(function ProposedPlanCard({30export const ProposedPlanCard = memo(function ProposedPlanCard({
30 planMarkdown,31 planMarkdown,
...@@ -39,6 +40,15 @@ export const ProposedPlanCard = memo(function ProposedPlanCard({...@@ -39,6 +40,15 @@ export const ProposedPlanCard = memo(function ProposedPlanCard({
39 const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);40 const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);
40 const [savePath, setSavePath] = useState("");41 const [savePath, setSavePath] = useState("");
41 const [isSavingToWorkspace, setIsSavingToWorkspace] = useState(false);42 const [isSavingToWorkspace, setIsSavingToWorkspace] = useState(false);
43 const { copyToClipboard, isCopied } = useCopyToClipboard({
44 onError: (error) => {
45 toastManager.add({
46 type: "error",
47 title: "Could not copy plan",
48 description: error instanceof Error ? error.message : "An error occurred while copying.",
49 });
50 },
51 });
42 const savePathInputId = useId();52 const savePathInputId = useId();
43 const title = proposedPlanTitle(planMarkdown) ?? "Proposed plan";53 const title = proposedPlanTitle(planMarkdown) ?? "Proposed plan";
44 const lineCount = planMarkdown.split("\n").length;54 const lineCount = planMarkdown.split("\n").length;
...@@ -54,6 +64,10 @@ export const ProposedPlanCard = memo(function ProposedPlanCard({...@@ -54,6 +64,10 @@ export const ProposedPlanCard = memo(function ProposedPlanCard({
54 downloadPlanAsTextFile(downloadFilename, saveContents);64 downloadPlanAsTextFile(downloadFilename, saveContents);
55 };65 };
5666
67 const handleCopyPlan = () => {
68 copyToClipboard(saveContents);
69 };
70
57 const openSaveDialog = () => {71 const openSaveDialog = () => {
58 if (!workspaceRoot) {72 if (!workspaceRoot) {
59 toastManager.add({73 toastManager.add({
...@@ -127,6 +141,9 @@ export const ProposedPlanCard = memo(function ProposedPlanCard({...@@ -127,6 +141,9 @@ export const ProposedPlanCard = memo(function ProposedPlanCard({
127 <EllipsisIcon aria-hidden="true" className="size-4" />141 <EllipsisIcon aria-hidden="true" className="size-4" />
128 </MenuTrigger>142 </MenuTrigger>
129 <MenuPopup align="end">143 <MenuPopup align="end">
144 <MenuItem onClick={handleCopyPlan}>
145 {isCopied ? "Copied!" : "Copy to clipboard"}
146 </MenuItem>
130 <MenuItem onClick={handleDownload}>Download as markdown</MenuItem>147 <MenuItem onClick={handleDownload}>Download as markdown</MenuItem>
131 <MenuItem onClick={openSaveDialog} disabled={!workspaceRoot || isSavingToWorkspace}>148 <MenuItem onClick={openSaveDialog} disabled={!workspaceRoot || isSavingToWorkspace}>
132 Save to workspace149 Save to workspace