fix(site): show accurate message and warning color for startup script failures in agent row (#23654)

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. 🧑‍💻
This commit is contained in:
Atif Ali
2026-03-31 12:04:51 +00:00
committed by GitHub
parent af678606fc
commit e3e17e15f7
4 changed files with 343 additions and 169 deletions
+2 -2
View File
@@ -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",
};
@@ -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<typeof AgentStatus> = {
title: "modules/resources/AgentStatus",
component: AgentStatus,
};
export default meta;
type Story = StoryObj<typeof AgentStatus>;
/**
* 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",
},
},
};
+106 -155
View File
@@ -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<AgentWarningTooltipProps> = ({
ariaLabel,
title,
detail,
troubleshootingURL,
variant = "warning",
}) => {
return (
<HelpTooltip>
<HelpTooltipTrigger asChild role="status" aria-label={ariaLabel}>
<TriangleAlertIcon
className={cn(
"relative size-3.5",
variant === "warning"
? "text-content-warning"
: "text-content-destructive",
)}
/>
</HelpTooltipTrigger>
<HelpTooltipContent>
<HelpTooltipTitle>{title}</HelpTooltipTitle>
<HelpTooltipText>
{detail}
{troubleshootingURL && (
<>
{" "}
<Link target="_blank" rel="noreferrer" href={troubleshootingURL}>
Troubleshoot
</Link>
</>
)}
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
);
};
const ReadyLifecycle: FC = () => {
return (
<div
@@ -66,54 +121,24 @@ interface DevcontainerStatusProps {
agent?: WorkspaceAgent;
}
const StartTimeoutLifecycle: FC<AgentStatusProps> = ({ agent }) => {
return (
<HelpTooltip>
<HelpTooltipTrigger asChild role="status" aria-label="Agent timeout">
<TriangleAlertIcon css={styles.timeoutWarning} />
</HelpTooltipTrigger>
const StartTimeoutLifecycle: FC<AgentStatusProps> = ({ agent }) => (
<AgentWarningTooltip
ariaLabel="Startup script timeout"
title={agentScriptMessages.start_timeout.title}
detail={agentScriptMessages.start_timeout.detail}
troubleshootingURL={agent.troubleshooting_url}
/>
);
<HelpTooltipContent>
<HelpTooltipTitle>Agent is taking too long to start</HelpTooltipTitle>
<HelpTooltipText>
We noticed this agent is taking longer than expected to start.{" "}
<Link
target="_blank"
rel="noreferrer"
href={agent.troubleshooting_url}
>
Troubleshoot
</Link>
.
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
);
};
const StartErrorLifecycle: FC<AgentStatusProps> = ({ agent }) => {
return (
<HelpTooltip>
<HelpTooltipTrigger asChild role="status" aria-label="Start error">
<TriangleAlertIcon css={styles.errorWarning} />
</HelpTooltipTrigger>
<HelpTooltipContent>
<HelpTooltipTitle>Error starting the agent</HelpTooltipTitle>
<HelpTooltipText>
Something went wrong during the agent startup.{" "}
<Link
target="_blank"
rel="noreferrer"
href={agent.troubleshooting_url}
>
Troubleshoot
</Link>
.
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
);
};
const StartErrorLifecycle: FC<AgentStatusProps> = ({ agent }) => (
<AgentWarningTooltip
ariaLabel="Startup script failed"
title={agentScriptMessages.start_error.title}
detail={agentScriptMessages.start_error.detail}
troubleshootingURL={agent.troubleshooting_url}
variant="warning"
/>
);
const ShuttingDownLifecycle: FC = () => {
return (
@@ -130,53 +155,24 @@ const ShuttingDownLifecycle: FC = () => {
);
};
const ShutdownTimeoutLifecycle: FC<AgentStatusProps> = ({ agent }) => {
return (
<HelpTooltip>
<HelpTooltipTrigger asChild role="status" aria-label="Stop timeout">
<TriangleAlertIcon css={styles.timeoutWarning} />
</HelpTooltipTrigger>
<HelpTooltipContent>
<HelpTooltipTitle>Agent is taking too long to stop</HelpTooltipTitle>
<HelpTooltipText>
We noticed this agent is taking longer than expected to stop.{" "}
<Link
target="_blank"
rel="noreferrer"
href={agent.troubleshooting_url}
>
Troubleshoot
</Link>
.
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
);
};
const ShutdownTimeoutLifecycle: FC<AgentStatusProps> = ({ agent }) => (
<AgentWarningTooltip
ariaLabel="Shutdown script timeout"
title={agentScriptMessages.shutdown_timeout.title}
detail={agentScriptMessages.shutdown_timeout.detail}
troubleshootingURL={agent.troubleshooting_url}
/>
);
const ShutdownErrorLifecycle: FC<AgentStatusProps> = ({ agent }) => {
return (
<HelpTooltip>
<HelpTooltipTrigger asChild role="status" aria-label="Stop error">
<TriangleAlertIcon css={styles.errorWarning} />
</HelpTooltipTrigger>
<HelpTooltipContent>
<HelpTooltipTitle>Error stopping the agent</HelpTooltipTitle>
<HelpTooltipText>
Something went wrong while trying to stop the agent.{" "}
<Link
target="_blank"
rel="noreferrer"
href={agent.troubleshooting_url}
>
Troubleshoot
</Link>
.
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
);
};
const ShutdownErrorLifecycle: FC<AgentStatusProps> = ({ agent }) => (
<AgentWarningTooltip
ariaLabel="Shutdown script failed"
title={agentScriptMessages.shutdown_error.title}
detail={agentScriptMessages.shutdown_error.detail}
troubleshootingURL={agent.troubleshooting_url}
variant="warning"
/>
);
const OffLifecycle: FC = () => {
return (
@@ -259,29 +255,14 @@ const ConnectingStatus: FC = () => {
);
};
const TimeoutStatus: FC<AgentStatusProps> = ({ agent }) => {
return (
<HelpTooltip>
<HelpTooltipTrigger asChild role="status" aria-label="Timeout">
<TriangleAlertIcon css={styles.timeoutWarning} />
</HelpTooltipTrigger>
<HelpTooltipContent>
<HelpTooltipTitle>Agent is taking too long to connect</HelpTooltipTitle>
<HelpTooltipText>
We noticed this agent is taking longer than expected to connect.{" "}
<Link
target="_blank"
rel="noreferrer"
href={agent.troubleshooting_url}
>
Troubleshoot
</Link>
.
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
);
};
const TimeoutStatus: FC<AgentStatusProps> = ({ agent }) => (
<AgentWarningTooltip
ariaLabel="Timeout"
title={agentConnectionMessages.timeout.title}
detail={agentConnectionMessages.timeout.detail}
troubleshootingURL={agent.troubleshooting_url}
/>
);
export const AgentStatus: FC<AgentStatusProps> = ({ agent }) => {
return (
@@ -324,31 +305,15 @@ const SubAgentStatus: FC<SubAgentStatusProps> = ({ agent }) => {
);
};
const DevcontainerStartError: FC<AgentStatusProps> = ({ agent }) => {
return (
<HelpTooltip>
<HelpTooltipTrigger asChild role="status" aria-label="Start error">
<TriangleAlertIcon css={styles.errorWarning} />
</HelpTooltipTrigger>
<HelpTooltipContent>
<HelpTooltipTitle>
Error starting the devcontainer agent
</HelpTooltipTitle>
<HelpTooltipText>
Something went wrong during the devcontainer agent startup.{" "}
<Link
target="_blank"
rel="noreferrer"
href={agent.troubleshooting_url}
>
Troubleshoot
</Link>
.
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
);
};
const DevcontainerStartError: FC<AgentStatusProps> = ({ agent }) => (
<AgentWarningTooltip
ariaLabel="Start error"
title="Error starting the devcontainer agent"
detail="Something went wrong during the devcontainer agent startup."
troubleshootingURL={agent.troubleshooting_url}
variant="error"
/>
);
export const DevcontainerStatus: FC<DevcontainerStatusProps> = ({
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<string, Interpolation<Theme>>;
+54 -12
View File
@@ -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,
};