From e3e17e15f72335e84da224d3222ac7bd92081bdc Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 31 Mar 2026 12:04:51 +0000 Subject: [PATCH] fix(site): show accurate message and warning color for startup script failures in agent row (#23654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent row tooltip showed "Error starting the agent" / "Something went wrong during the agent startup" with a red border when a startup script fails. This is misleading — the agent is started and functional, only the startup script exited with a non-zero code. Extracts shared message constants (`agentLifecycleMessages`, `agentStatusMessages`) from `health.ts` so both the workspace-level health classification and the per-agent-row tooltips reference the same single source of truth. No more duplicated wording that can drift. Changes: - **`health.ts`**: Exports `agentLifecycleMessages` and `agentStatusMessages` maps; `getAgentHealthIssue` now references them instead of inline strings. - **`AgentStatus.tsx`**: All lifecycle/status tooltip components (`StartErrorLifecycle`, `StartTimeoutLifecycle`, `ShutdownTimeoutLifecycle`, `ShutdownErrorLifecycle`, `TimeoutStatus`) now import and render from the shared message constants. `StartErrorLifecycle` icon changed from red (`errorWarning`) to orange (`timeoutWarning`). - **`AgentRow.tsx`**: `start_error` border changed from `border-border-destructive` (red) to `border-border-warning` (orange). Closes #23652 Refs #21389 > 🤖 This PR was created with the help of Coder Agents, and has been reviewed by my human. 🧑‍💻 --- site/src/modules/resources/AgentRow.tsx | 4 +- .../modules/resources/AgentStatus.stories.tsx | 181 ++++++++++++ site/src/modules/resources/AgentStatus.tsx | 261 +++++++----------- site/src/modules/workspaces/health.ts | 66 ++++- 4 files changed, 343 insertions(+), 169 deletions(-) create mode 100644 site/src/modules/resources/AgentStatus.stories.tsx diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index d3482eaeda..193fa9e593 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -68,8 +68,8 @@ const statusBorderClassByLifecycle: Partial< ready: "border-border-success", start_timeout: "border-border-warning", shutdown_timeout: "border-border-warning", - start_error: "border-border-destructive", - shutdown_error: "border-border-destructive", + start_error: "border-border-warning", + shutdown_error: "border-border-warning", off: "border-border", }; diff --git a/site/src/modules/resources/AgentStatus.stories.tsx b/site/src/modules/resources/AgentStatus.stories.tsx new file mode 100644 index 0000000000..17590d1078 --- /dev/null +++ b/site/src/modules/resources/AgentStatus.stories.tsx @@ -0,0 +1,181 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, screen, userEvent, waitFor, within } from "storybook/test"; +import { MockWorkspaceAgent } from "#/testHelpers/entities"; +import { + agentConnectionMessages, + agentScriptMessages, +} from "../workspaces/health"; +import { AgentStatus } from "./AgentStatus"; + +const meta: Meta = { + title: "modules/resources/AgentStatus", + component: AgentStatus, +}; + +export default meta; +type Story = StoryObj; + +/** + * Shared play helper that hovers the status icon, then asserts the + * tooltip contains the expected title and detail text, plus a + * troubleshoot link when the agent has a troubleshooting URL. + */ +async function expectTooltip( + ariaLabel: string, + title: string, + detail: string, + hasTroubleshootLink: boolean, +) { + const icon = screen.getByRole("status", { name: ariaLabel }); + await userEvent.hover(icon); + await waitFor(() => { + const tooltip = screen.getByRole("tooltip"); + expect(tooltip).toHaveTextContent(title); + expect(tooltip).toHaveTextContent(detail); + if (hasTroubleshootLink) { + expect( + within(tooltip).getByRole("link", { name: "Troubleshoot" }), + ).toBeInTheDocument(); + } else { + expect( + within(tooltip).queryByRole("link", { name: "Troubleshoot" }), + ).not.toBeInTheDocument(); + } + }); +} + +export const Ready: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "connected", + lifecycle_state: "ready", + }, + }, +}; + +export const StartupScriptFailed: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "connected", + lifecycle_state: "start_error", + }, + }, + play: async () => { + await expectTooltip( + "Startup script failed", + agentScriptMessages.start_error.title, + agentScriptMessages.start_error.detail, + true, + ); + }, +}; + +export const StartupScriptTimeout: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "connected", + lifecycle_state: "start_timeout", + }, + }, + play: async () => { + await expectTooltip( + "Startup script timeout", + agentScriptMessages.start_timeout.title, + agentScriptMessages.start_timeout.detail, + true, + ); + }, +}; + +export const ShutdownScriptFailed: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "connected", + lifecycle_state: "shutdown_error", + }, + }, + play: async () => { + await expectTooltip( + "Shutdown script failed", + agentScriptMessages.shutdown_error.title, + agentScriptMessages.shutdown_error.detail, + true, + ); + }, +}; + +export const ShutdownScriptTimeout: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "connected", + lifecycle_state: "shutdown_timeout", + }, + }, + play: async () => { + await expectTooltip( + "Shutdown script timeout", + agentScriptMessages.shutdown_timeout.title, + agentScriptMessages.shutdown_timeout.detail, + true, + ); + }, +}; + +export const ConnectionTimeout: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "timeout", + }, + }, + play: async () => { + await expectTooltip( + "Timeout", + agentConnectionMessages.timeout.title, + agentConnectionMessages.timeout.detail, + true, + ); + }, +}; + +export const StartupScriptFailedNoTroubleshootURL: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "connected", + lifecycle_state: "start_error", + troubleshooting_url: "", + }, + }, + play: async () => { + await expectTooltip( + "Startup script failed", + agentScriptMessages.start_error.title, + agentScriptMessages.start_error.detail, + false, + ); + }, +}; + +export const Disconnected: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "disconnected", + }, + }, +}; + +export const Connecting: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "connecting", + }, + }, +}; diff --git a/site/src/modules/resources/AgentStatus.tsx b/site/src/modules/resources/AgentStatus.tsx index 2f6fc98fc5..d6f185b233 100644 --- a/site/src/modules/resources/AgentStatus.tsx +++ b/site/src/modules/resources/AgentStatus.tsx @@ -19,6 +19,11 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; +import { cn } from "#/utils/cn"; +import { + agentConnectionMessages, + agentScriptMessages, +} from "../workspaces/health"; // If we think in the agent status and lifecycle into a single enum/state I'd // say we would have: connecting, timeout, disconnected, connected:created, @@ -26,6 +31,56 @@ import { // connected:ready, connected:shutting_down, connected:shutdown_timeout, // connected:shutdown_error, connected:off. +interface AgentWarningTooltipProps { + ariaLabel: string; + title: string; + detail: string; + troubleshootingURL?: string; + variant?: "warning" | "error"; +} + +/** + * Shared tooltip for agent warning/error states. Renders an alert + * icon with a help tooltip showing the title, detail, and an + * optional troubleshooting link. + */ +const AgentWarningTooltip: FC = ({ + ariaLabel, + title, + detail, + troubleshootingURL, + variant = "warning", +}) => { + return ( + + + + + + {title} + + {detail} + {troubleshootingURL && ( + <> + {" "} + + Troubleshoot + + + )} + + + + ); +}; + const ReadyLifecycle: FC = () => { return (
= ({ agent }) => { - return ( - - - - +const StartTimeoutLifecycle: FC = ({ agent }) => ( + +); - - Agent is taking too long to start - - We noticed this agent is taking longer than expected to start.{" "} - - Troubleshoot - - . - - - - ); -}; - -const StartErrorLifecycle: FC = ({ agent }) => { - return ( - - - - - - Error starting the agent - - Something went wrong during the agent startup.{" "} - - Troubleshoot - - . - - - - ); -}; +const StartErrorLifecycle: FC = ({ agent }) => ( + +); const ShuttingDownLifecycle: FC = () => { return ( @@ -130,53 +155,24 @@ const ShuttingDownLifecycle: FC = () => { ); }; -const ShutdownTimeoutLifecycle: FC = ({ agent }) => { - return ( - - - - - - Agent is taking too long to stop - - We noticed this agent is taking longer than expected to stop.{" "} - - Troubleshoot - - . - - - - ); -}; +const ShutdownTimeoutLifecycle: FC = ({ agent }) => ( + +); -const ShutdownErrorLifecycle: FC = ({ agent }) => { - return ( - - - - - - Error stopping the agent - - Something went wrong while trying to stop the agent.{" "} - - Troubleshoot - - . - - - - ); -}; +const ShutdownErrorLifecycle: FC = ({ agent }) => ( + +); const OffLifecycle: FC = () => { return ( @@ -259,29 +255,14 @@ const ConnectingStatus: FC = () => { ); }; -const TimeoutStatus: FC = ({ agent }) => { - return ( - - - - - - Agent is taking too long to connect - - We noticed this agent is taking longer than expected to connect.{" "} - - Troubleshoot - - . - - - - ); -}; +const TimeoutStatus: FC = ({ agent }) => ( + +); export const AgentStatus: FC = ({ agent }) => { return ( @@ -324,31 +305,15 @@ const SubAgentStatus: FC = ({ agent }) => { ); }; -const DevcontainerStartError: FC = ({ agent }) => { - return ( - - - - - - - Error starting the devcontainer agent - - - Something went wrong during the devcontainer agent startup.{" "} - - Troubleshoot - - . - - - - ); -}; +const DevcontainerStartError: FC = ({ agent }) => ( + +); export const DevcontainerStatus: FC = ({ devcontainer, @@ -398,18 +363,4 @@ const styles = { backgroundColor: theme.palette.info.light, animation: "$pulse 1.5s 0.5s ease-in-out forwards infinite", }), - - timeoutWarning: (theme) => ({ - color: theme.palette.warning.light, - width: 14, - height: 14, - position: "relative", - }), - - errorWarning: (theme) => ({ - color: theme.palette.error.main, - width: 14, - height: 14, - position: "relative", - }), } satisfies Record>; diff --git a/site/src/modules/workspaces/health.ts b/site/src/modules/workspaces/health.ts index 29bd9a1862..cebe53a9b8 100644 --- a/site/src/modules/workspaces/health.ts +++ b/site/src/modules/workspaces/health.ts @@ -1,5 +1,51 @@ import type { Workspace, WorkspaceAgentStatus } from "#/api/typesGenerated"; +/** + * Canonical messages for startup and shutdown script issues. + * Used by the per-agent-row tooltips in AgentStatus; the + * start-related entries are also shared with the workspace-level + * health classification in getAgentHealthIssue. + */ +export const agentScriptMessages = { + start_error: { + title: "Startup script failed", + detail: + "A startup script exited with an error. Check the agent logs for details.", + }, + start_timeout: { + title: "Startup script is taking longer than expected", + detail: + "A startup script has exceeded the expected time. Check the agent logs for details.", + }, + shutdown_error: { + title: "Shutdown script failed", + detail: + "A shutdown script exited with an error. Check the agent logs for details.", + }, + shutdown_timeout: { + title: "Shutdown script is taking longer than expected", + detail: + "A shutdown script has exceeded the expected time. Check the agent logs for details.", + }, +} as const; + +/** + * Canonical messages for agent connection issues (the agent + * process connecting to the Coder control plane). + */ +export const agentConnectionMessages = { + timeout: { + title: "Agent is taking longer than expected to connect", + detail: + "Continue to wait and check the log output for errors. If agents do not connect, try restarting the workspace.", + }, + disconnected: { + title: "Workspace agent has disconnected", + detail: + "Check the log output for errors. If agents do not reconnect, try restarting the workspace.", + }, +} as const; + interface AgentHealthIssue { title: string; detail: string; @@ -53,9 +99,8 @@ export function getAgentHealthIssue(workspace: Workspace): AgentHealthIssue { return { title: plural ? `${failingAgentCount} workspace agents have disconnected` - : "Workspace agent has disconnected", - detail: - "Check the log output for errors. If agents do not reconnect, try restarting the workspace.", + : agentConnectionMessages.disconnected.title, + detail: agentConnectionMessages.disconnected.detail, severity: "warning", prominent: true, }; @@ -65,9 +110,8 @@ export function getAgentHealthIssue(workspace: Workspace): AgentHealthIssue { return { title: plural ? `${failingAgentCount} agents are taking longer than expected to connect` - : "Agent is taking longer than expected to connect", - detail: - "Continue to wait and check the log output for errors. If agents do not connect, try restarting the workspace.", + : agentConnectionMessages.timeout.title, + detail: agentConnectionMessages.timeout.detail, severity: "warning", prominent: false, }; @@ -88,9 +132,8 @@ export function getAgentHealthIssue(workspace: Workspace): AgentHealthIssue { return { title: plural ? `Startup scripts failed on ${failingAgentCount} agents` - : "Startup script failed", - detail: - "A startup script exited with an error. Check the agent logs for details.", + : agentScriptMessages.start_error.title, + detail: agentScriptMessages.start_error.detail, severity: "warning", prominent: true, }; @@ -105,9 +148,8 @@ export function getAgentHealthIssue(workspace: Workspace): AgentHealthIssue { return { title: plural ? `Startup scripts are taking longer than expected on ${failingAgentCount} agents` - : "Startup script is taking longer than expected", - detail: - "A startup script has exceeded the expected time. Check the agent logs for details.", + : agentScriptMessages.start_timeout.title, + detail: agentScriptMessages.start_timeout.detail, severity: "warning", prominent: false, };