feat(site): display devcontainer start error (#18637)

Fixes https://github.com/coder/internal/issues/705

Surface errors on the UI when a devcontainer agent is unable to be
injected.
This commit is contained in:
Danielle Maywood
2025-06-30 21:34:29 +01:00
committed by GitHub
parent fc7700a62f
commit 4756080eb2
12 changed files with 325 additions and 5 deletions
+1
View File
@@ -3329,6 +3329,7 @@ export interface WorkspaceAgentDevcontainer {
readonly dirty: boolean;
readonly container?: WorkspaceAgentContainer;
readonly agent?: WorkspaceAgentDevcontainerAgent;
readonly error?: string;
}
// From codersdk/workspaceagents.go
@@ -46,6 +46,16 @@ const meta: Meta<typeof AgentDevcontainerCard> = {
export default meta;
type Story = StoryObj<typeof AgentDevcontainerCard>;
export const HasError: Story = {
args: {
devcontainer: {
...MockWorkspaceAgentDevcontainer,
error: "unable to inject devcontainer with agent",
agent: undefined,
},
},
};
export const NoPorts: Story = {};
export const WithPorts: Story = {
@@ -6,6 +6,7 @@ import type {
WorkspaceAgentDevcontainer,
WorkspaceAgentListContainersResponse,
} from "api/typesGenerated";
import { Button } from "components/Button/Button";
import { displayError } from "components/GlobalSnackbar/utils";
import { Spinner } from "components/Spinner/Spinner";
@@ -23,11 +24,12 @@ import { AppStatuses } from "pages/WorkspacePage/AppStatuses";
import type { FC } from "react";
import { useEffect } from "react";
import { useMutation, useQueryClient } from "react-query";
import { cn } from "utils/cn";
import { portForwardURL } from "utils/portForward";
import { AgentApps, organizeAgentApps } from "./AgentApps/AgentApps";
import { AgentButton } from "./AgentButton";
import { AgentLatency } from "./AgentLatency";
import { SubAgentStatus } from "./AgentStatus";
import { DevcontainerStatus } from "./AgentStatus";
import { PortForwardButton } from "./PortForwardButton";
import { AgentSSHButton } from "./SSHButton/SSHButton";
import { SubAgentOutdatedTooltip } from "./SubAgentOutdatedTooltip";
@@ -190,7 +192,10 @@ export const AgentDevcontainerCard: FC<AgentDevcontainerCardProps> = ({
key={devcontainer.id}
direction="column"
spacing={0}
className="relative py-4 border border-dashed border-border rounded"
className={cn(
"relative py-4 border border-dashed border-border rounded",
devcontainer.error && "border-content-destructive border-solid",
)}
>
<div
className="absolute -top-2 left-5
@@ -208,7 +213,11 @@ export const AgentDevcontainerCard: FC<AgentDevcontainerCardProps> = ({
>
<div className="flex items-center gap-6 text-xs text-content-secondary">
<div className="flex items-center gap-4 md:w-full">
<SubAgentStatus agent={subAgent} />
<DevcontainerStatus
devcontainer={devcontainer}
parentAgent={parentAgent}
agent={subAgent}
/>
<span
className="max-w-xs shrink-0
overflow-hidden text-ellipsis whitespace-nowrap
@@ -273,6 +282,12 @@ export const AgentDevcontainerCard: FC<AgentDevcontainerCardProps> = ({
</div>
</header>
{devcontainer.error && (
<div className="px-8 pt-2 text-xs text-content-destructive">
{devcontainer.error}
</div>
)}
{(showSubAgentApps || showSubAgentAppsPlaceholders) && (
<div className="flex flex-col gap-8 px-8 pt-4">
{subAgent &&
+8
View File
@@ -156,6 +156,9 @@ export const AgentRow: FC<AgentRowProps> = ({
shouldDisplayAppsSection = false;
}
// Check if any devcontainers have errors to gray out agent border
const hasDevcontainerErrors = devcontainers?.some((dc) => dc.error);
return (
<Stack
key={agent.id}
@@ -165,6 +168,7 @@ export const AgentRow: FC<AgentRowProps> = ({
styles.agentRow,
styles[`agentRow-${agent.status}`],
styles[`agentRow-lifecycle-${agent.lifecycle_state}`],
hasDevcontainerErrors && styles.agentRowWithErrors,
]}
>
<header css={styles.header}>
@@ -537,4 +541,8 @@ const styles = {
position: "relative",
},
}),
agentRowWithErrors: (theme) => ({
borderColor: theme.palette.divider,
}),
} satisfies Record<string, Interpolation<Theme>>;
+52 -2
View File
@@ -1,7 +1,10 @@
import type { Interpolation, Theme } from "@emotion/react";
import Link from "@mui/material/Link";
import Tooltip from "@mui/material/Tooltip";
import type { WorkspaceAgent } from "api/typesGenerated";
import type {
WorkspaceAgent,
WorkspaceAgentDevcontainer,
} from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import {
HelpTooltip,
@@ -50,6 +53,12 @@ interface SubAgentStatusProps {
agent?: WorkspaceAgent;
}
interface DevcontainerStatusProps {
devcontainer: WorkspaceAgentDevcontainer;
parentAgent: WorkspaceAgent;
agent?: WorkspaceAgent;
}
const StartTimeoutLifecycle: FC<AgentStatusProps> = ({ agent }) => {
return (
<HelpTooltip>
@@ -274,7 +283,7 @@ export const AgentStatus: FC<AgentStatusProps> = ({ agent }) => {
);
};
export const SubAgentStatus: FC<SubAgentStatusProps> = ({ agent }) => {
const SubAgentStatus: FC<SubAgentStatusProps> = ({ agent }) => {
if (!agent) {
return <DisconnectedStatus />;
}
@@ -296,6 +305,47 @@ export const SubAgentStatus: FC<SubAgentStatusProps> = ({ agent }) => {
);
};
const DevcontainerStartError: FC<AgentStatusProps> = ({ agent }) => {
return (
<HelpTooltip>
<PopoverTrigger role="status" aria-label="Start error">
<TriangleAlertIcon css={styles.errorWarning} />
</PopoverTrigger>
<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>
);
};
export const DevcontainerStatus: FC<DevcontainerStatusProps> = ({
devcontainer,
parentAgent,
agent,
}) => {
if (devcontainer.error) {
// When a dev container has an 'error' associated with it,
// then we won't have an agent associated with it. This is
// why we use the parent agent instead of the sub agent.
return <DevcontainerStartError agent={parentAgent} />;
}
return <SubAgentStatus agent={agent} />;
};
const styles = {
status: {
width: 6,