fix(site): refine execute tool transcript UI (#25432)

This commit is contained in:
Danielle Maywood
2026-05-18 14:44:43 +01:00
committed by GitHub
parent c69dd9c5dc
commit 46821525f7
4 changed files with 77 additions and 79 deletions
@@ -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(() => {
@@ -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) => (
<div className="w-72">
<Story />
</div>
),
],
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);
},
};
@@ -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<ExecuteToolInnerProps> = ({
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<ExecuteToolInnerProps> = ({
}
return (
<div className="group/exec grid w-full grid-cols-[minmax(0,1fr)_auto] items-start gap-x-2 rounded-md bg-surface-primary font-mono font-normal text-xs leading-5">
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
{hasOutput ? (
<button
type="button"
aria-expanded={outputOpen}
aria-label={outputToggleLabel}
onClick={() => setOutputOpen((value) => !value)}
className="col-start-1 row-start-1 m-0 flex w-full min-w-0 cursor-pointer items-center gap-2 border-0 bg-transparent p-0 text-left font-[inherit] font-normal text-[inherit] text-content-secondary transition-colors hover:text-content-primary"
>
<ShellCommandLine
command={command}
durationLabel={durationLabel}
expanded={outputOpen}
/>
</button>
) : (
<div className="col-start-1 row-start-1 flex min-w-0 items-center gap-2 font-normal text-content-secondary">
<ShellCommandLine
command={command}
durationLabel={durationLabel}
/>
</div>
)}
</TooltipTrigger>
<TooltipContent className="max-w-xl whitespace-pre-wrap break-all font-mono font-normal">
{command}
</TooltipContent>
</Tooltip>
<div className="group/exec grid w-full grid-cols-[minmax(0,1fr)_auto] items-start gap-x-2 rounded-md bg-surface-primary font-sans font-normal text-xs leading-5">
<button
type="button"
aria-expanded={outputOpen}
aria-label={outputToggleLabel}
onClick={() => setOutputOpen((value) => !value)}
className="col-start-1 row-start-1 m-0 flex w-full min-w-0 cursor-pointer items-center gap-2 border-0 bg-transparent p-0 text-left font-[inherit] font-normal text-[inherit] text-content-secondary transition-colors hover:text-content-primary"
>
<ShellCommandLine
command={command}
durationLabel={durationLabel}
expanded={outputOpen}
/>
</button>
<div className="col-start-2 row-start-1 flex shrink-0 items-center gap-1">
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
@@ -174,8 +154,12 @@ const ExecuteToolInner: React.FC<ExecuteToolInnerProps> = ({
className="-my-0.5 size-6 p-0 opacity-0 transition-opacity hover:bg-surface-tertiary group-hover/exec:opacity-100"
/>
</div>
{hasOutput && outputOpen && (
<ShellOutputBody output={output} isError={isError} />
{outputOpen && (
<ShellTranscriptBody
command={command}
output={output}
isError={isError}
/>
)}
</div>
);
@@ -188,9 +172,8 @@ const ShellCommandLine: React.FC<{
}> = ({ command, durationLabel, expanded }) => {
return (
<>
<TerminalIcon aria-hidden className="size-3.5 shrink-0 text-current" />
<span className="block min-w-0 truncate text-[13px] font-normal text-current">
{command}
Ran {command}
</span>
{durationLabel && (
<span className="shrink-0 text-[13px] font-normal text-content-secondary">
@@ -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 (
<ScrollArea
className="col-start-1 col-span-2 mt-1 rounded-md border border-solid border-border-default/50 bg-surface-secondary/30 text-2xs"
className="col-start-1 col-span-2 mt-2 rounded-xl bg-surface-secondary/60 text-2xs"
viewportClassName="max-h-64"
scrollBarClassName="w-1.5"
>
<pre
className={cn(
"m-0 whitespace-pre-wrap break-all border-0 bg-transparent px-2 py-1.5 font-mono text-xs leading-5",
isError ? "text-content-destructive" : "text-content-secondary",
<div className="px-3 py-2.5">
<pre className="m-0 whitespace-pre-wrap break-words border-0 bg-transparent p-0 font-mono text-xs font-semibold leading-5 text-content-primary">
<span aria-hidden className="select-none">
$
</span>{" "}
{command}
</pre>
{output.length > 0 && (
<pre
className={cn(
"m-0 mt-4 whitespace-pre-wrap break-words border-0 bg-transparent p-0 font-mono text-xs font-normal leading-5",
isError ? "text-content-destructive" : "text-content-secondary",
)}
>
{output}
</pre>
)}
>
{output}
</pre>
</div>
</ScrollArea>
);
};
@@ -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();