From aa5ec0bfcccdab37f2b57e7f9d15e96bea52a051 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 2 Apr 2026 14:30:36 +0300 Subject: [PATCH] feat(site/src/pages/AgentsPage): add copy button to ProposePlanTool (#23940) Reuse the CopyButton component to let users copy plan content from the propose_plan tool output. Follows the same pattern used by the assistant message copy button. --- .../tools/ProposePlanTool.stories.tsx | 33 ++++++++++++++++++- .../ChatElements/tools/ProposePlanTool.tsx | 8 ++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.stories.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.stories.tsx index 40a690383d..ecb13ad723 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { expect, spyOn, within } from "storybook/test"; +import { expect, spyOn, userEvent, within } from "storybook/test"; import { reactRouterParameters } from "storybook-addon-remix-react-router"; import { API } from "#/api/api"; import { Tool } from "./Tool"; @@ -79,6 +79,9 @@ export const Completed: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(await canvas.findByText("Implementation Plan")).toBeInTheDocument(); + expect( + canvas.getByRole("button", { name: "Copy plan" }), + ).toBeInTheDocument(); }, }; @@ -100,6 +103,34 @@ export const CustomPath: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(await canvas.findByText("Implementation Plan")).toBeInTheDocument(); + expect( + canvas.getByRole("button", { name: "Copy plan" }), + ).toBeInTheDocument(); + }, +}; + +export const CompletedCopyButton: Story = { + args: { + status: "completed", + args: { path: "/home/coder/PLAN.md" }, + result: { + ok: true, + path: "/home/coder/PLAN.md", + kind: "plan", + file_id: "test-file-id-copy", + media_type: "text/markdown", + }, + }, + beforeEach: () => { + spyOn(API.experimental, "getChatFileText").mockResolvedValue(samplePlan); + spyOn(navigator.clipboard, "writeText").mockResolvedValue(undefined); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.findByText("Implementation Plan"); + const copyBtn = canvas.getByRole("button", { name: "Copy plan" }); + await userEvent.click(copyBtn); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(samplePlan); }, }; diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx index 82fa5bec79..6c1be22845 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx @@ -2,6 +2,7 @@ import { LoaderIcon, TriangleAlertIcon } from "lucide-react"; import type React from "react"; import { useQuery } from "react-query"; import { API } from "#/api/api"; +import { CopyButton } from "#/components/CopyButton/CopyButton"; import { Tooltip, TooltipContent, @@ -78,7 +79,12 @@ export const ProposePlanTool: React.FC<{ )} {displayContent ? ( - {displayContent} + <> + {displayContent} +
+ +
+ ) : ( !fetchLoading && !effectiveError && (