From 46821525f7d2f7466735463898fb6e054c169f85 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 18 May 2026 14:44:43 +0100 Subject: [PATCH] fix(site): refine execute tool transcript UI (#25432) --- .../ConversationTimeline.stories.tsx | 9 +- .../tools/ExecuteTool.stories.tsx | 36 ++++---- .../ChatElements/tools/ExecuteTool.tsx | 92 +++++++++---------- .../ChatElements/tools/Tool.stories.tsx | 19 ++-- 4 files changed, 77 insertions(+), 79 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx index ead619fb7e..d086f81eb8 100644 --- a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx @@ -2031,14 +2031,13 @@ export const ToolDisplayModesFromPreferences: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(canvas.getByText("pnpm test")).toBeVisible(); + const commandOutputButton = canvas.getByRole("button", { + name: "Expand command", + }); + expect(commandOutputButton).toHaveTextContent("Ran pnpm test"); expect(canvas.queryByText("tests passed")).not.toBeInTheDocument(); expect(canvas.getByText(/Edited config\.ts/)).toBeVisible(); expect(canvas.queryAllByTestId("edit-file-diff")).toHaveLength(0); - - const commandOutputButton = canvas.getByRole("button", { - name: "Expand command output", - }); expect(commandOutputButton).toHaveAttribute("aria-expanded", "false"); await userEvent.click(commandOutputButton); await waitFor(() => { diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.stories.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.stories.tsx index 2400d856ab..bfdf822268 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { expect, screen, userEvent, within } from "storybook/test"; +import { userEvent, within } from "storybook/test"; import { ExecuteTool } from "./ExecuteTool"; const longCommand = @@ -30,6 +30,13 @@ export const ShortCommand: Story = { command: "git status", output: "", }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const summaryButton = await canvas.findByRole("button", { + name: "Expand command", + }); + await userEvent.click(summaryButton); + }, }; export const RunningWithoutCommand: Story = { @@ -38,31 +45,26 @@ export const RunningWithoutCommand: Story = { status: "running", output: "", }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - expect(canvas.queryByText("$")).not.toBeInTheDocument(); - expect( - canvas.queryByRole("button", { name: "Copy command" }), - ).not.toBeInTheDocument(); - }, }; export const LongCommand: Story = { + decorators: [ + (Story) => ( +
+ +
+ ), + ], args: { command: longCommand, output: "", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const command = canvas.getByText(longCommand); - expect(command).toBeVisible(); - expect( - canvas.queryByRole("button", { name: longCommand }), - ).not.toBeInTheDocument(); - await userEvent.hover(command); - expect( - await screen.findByRole("tooltip", undefined, { timeout: 2000 }), - ).toHaveTextContent(longCommand); + const summaryButton = await canvas.findByRole("button", { + name: "Expand command", + }); + await userEvent.click(summaryButton); }, }; diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.tsx index 602053992c..6620a39229 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.tsx @@ -6,7 +6,6 @@ import { LayersIcon, LoaderIcon, OctagonXIcon, - TerminalIcon, TriangleAlertIcon, } from "lucide-react"; import type React from "react"; @@ -79,13 +78,10 @@ const ExecuteToolInner: React.FC = ({ outputInitiallyOpen, }) => { const hasCommand = command.trim().length > 0; - const hasOutput = output.length > 0; const isRunning = status === "running"; const showFailureIndicator = isError && !isRunning; const [outputOpen, setOutputOpen] = useState(outputInitiallyOpen); - const outputToggleLabel = outputOpen - ? "Collapse command output" - : "Expand command output"; + const outputToggleLabel = outputOpen ? "Collapse command" : "Expand command"; const durationLabel = formatShellDurationMs(durationMs); if (!hasCommand) { @@ -93,36 +89,20 @@ const ExecuteToolInner: React.FC = ({ } return ( -
- - - {hasOutput ? ( - - ) : ( -
- -
- )} -
- - {command} - -
+
+
{isRunning && ( @@ -174,8 +154,12 @@ const ExecuteToolInner: React.FC = ({ className="-my-0.5 size-6 p-0 opacity-0 transition-opacity hover:bg-surface-tertiary group-hover/exec:opacity-100" />
- {hasOutput && outputOpen && ( - + {outputOpen && ( + )}
); @@ -188,9 +172,8 @@ const ShellCommandLine: React.FC<{ }> = ({ command, durationLabel, expanded }) => { return ( <> - - {command} + Ran {command} {durationLabel && ( @@ -209,24 +192,35 @@ const ShellCommandLine: React.FC<{ ); }; -const ShellOutputBody: React.FC<{ +const ShellTranscriptBody: React.FC<{ + command: string; output: string; isError: boolean; -}> = ({ output, isError }) => { +}> = ({ command, output, isError }) => { return ( -
+				
+					
+						$
+					{" "}
+					{command}
+				
+ {output.length > 0 && ( +
+						{output}
+					
)} - > - {output} -
+
); }; diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.stories.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.stories.tsx index fc8b23793c..802d114658 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.stories.tsx @@ -99,7 +99,7 @@ export const ExecuteError: Story = { expect(canvas.getByRole("img", { name: "Command failed" })).toBeVisible(); expect(canvas.queryByText("exit 1")).not.toBeInTheDocument(); await userEvent.click( - canvas.getByRole("button", { name: "Expand command output" }), + canvas.getByRole("button", { name: "Expand command" }), ); await waitFor(() => { expect(canvas.getByText(/error line 1/)).toBeVisible(); @@ -144,13 +144,16 @@ export const ExecuteAlwaysCollapsed: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - expect(canvas.getByText(executeCommand)).toBeVisible(); + const commandButton = canvas.getByRole("button", { + name: "Expand command", + }); + expect(commandButton).toHaveTextContent(`Ran ${executeCommand}`); expect(canvas.queryByText("exit 0")).not.toBeInTheDocument(); expect(canvas.queryByText("2 lines")).not.toBeInTheDocument(); expect( canvas.queryByText(/From github\.com:coder\/coder/), ).not.toBeInTheDocument(); - await userEvent.click(canvas.getByText(executeCommand)); + await userEvent.click(commandButton); await waitFor(() => { expect(canvas.getByText(/From github\.com:coder\/coder/)).toBeVisible(); }); @@ -174,11 +177,11 @@ export const ExecuteLongCommandCollapsed: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const command = canvas.getByText(longExecuteCommand); - expect(command).toBeVisible(); - expect( - canvas.queryByRole("button", { name: longExecuteCommand }), - ).not.toBeInTheDocument(); + const commandButton = canvas.getByRole("button", { + name: "Expand command", + }); + expect(commandButton).toHaveTextContent(`Ran ${longExecuteCommand}`); + expect(commandButton).toHaveAttribute("aria-expanded", "false"); expect(canvas.queryByText("exit 0")).not.toBeInTheDocument(); expect(canvas.getByText("47.2s")).toBeVisible(); expect(canvas.queryByText("61 lines")).not.toBeInTheDocument();