fix(site): show startup script failure message without restart suggestion (#24449)

When a workspace agent's startup script fails, restarting the workspace
will not resolve the issue since the script will keep failing.
Previously all unhealthy workspaces showed the same generic notification
with a Restart button regardless of cause.

Now, when every failing agent has `lifecycle_state=start_error`, the
workspace-level notification shows "A startup script has failed" and
guides the user to contact their template admin instead of offering a
Restart action.

> Code written by Claude 🤖 reviewed by yours truly

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeremy Ruppel
2026-04-21 10:40:32 -04:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent ef2b3a7263
commit b4eb0e20e5
7 changed files with 235 additions and 37 deletions
@@ -98,6 +98,12 @@ const installScriptLogSource: WorkspaceAgentLogSource = {
display_name: "Install Script",
};
const startupScriptLogSource: WorkspaceAgentLogSource = {
...M.MockWorkspaceAgentLogSource,
id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
display_name: "Startup Script",
};
const tabbedLogs = [
{
id: 100,
@@ -229,6 +235,61 @@ export const StartError: Story = {
},
};
export const StartErrorWithTimings: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const scriptTab = await canvas.findByRole("tab", {
name: "Startup Script",
});
await waitFor(() =>
expect(scriptTab).toHaveAttribute("data-state", "active"),
);
},
args: {
agent: {
...M.MockWorkspaceAgentStartError,
logs_length: 2,
log_sources: [startupScriptLogSource],
},
agentScriptTimings: [
{
display_name: "Startup Script",
exit_code: 1,
stage: "start",
status: "exit_failure",
started_at: "2021-05-05T00:00:00.000Z",
ended_at: "2021-05-05T00:00:01.000Z",
workspace_agent_id: M.MockWorkspaceAgentStartError.id,
workspace_agent_name: M.MockWorkspaceAgentStartError.name,
},
],
},
parameters: {
webSocket: [
{
event: "message",
data: JSON.stringify([
{
id: 200,
level: "info",
output: "startup: preparing workspace",
source_id: M.MockWorkspaceAgentLogSource.id,
created_at: fixedLogTimestamp,
},
{
id: 201,
level: "error",
output: "startup script: command not found",
source_id: startupScriptLogSource.id,
created_at: fixedLogTimestamp,
},
]),
},
],
},
};
export const ShuttingDown: Story = {
args: {
agent: M.MockWorkspaceAgentShuttingDown,
+46 -10
View File
@@ -18,6 +18,7 @@ import { Link as RouterLink } from "react-router";
import AutoSizer from "react-virtualized-auto-sizer";
import type { FixedSizeList as List, ListOnScrollProps } from "react-window";
import type {
AgentScriptTiming,
Template,
Workspace,
WorkspaceAgent,
@@ -47,8 +48,14 @@ import { useKebabMenu } from "#/components/Tabs/utils/useKebabMenu";
import { useProxy } from "#/contexts/ProxyContext";
import { useClipboard } from "#/hooks/useClipboard";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { getAgentHealthIssues } from "#/modules/workspaces/health";
import { AgentAlert } from "#/pages/WorkspacePage/AgentAlert";
import {
agentScriptMessages,
getAgentHealthIssues,
} from "#/modules/workspaces/health";
import {
AgentAlert,
StartScriptFailureDetail,
} from "#/pages/WorkspacePage/AgentAlert";
import { AppStatuses } from "#/pages/WorkspacePage/AppStatuses";
import { cn } from "#/utils/cn";
import { AgentApps, organizeAgentApps } from "./AgentApps/AgentApps";
@@ -75,6 +82,7 @@ interface AgentRowProps {
workspace: Workspace;
template: Template;
initialMetadata?: WorkspaceAgentMetadata[];
agentScriptTimings?: readonly AgentScriptTiming[];
onUpdateAgent: () => void;
}
@@ -124,6 +132,7 @@ export const AgentRow: FC<AgentRowProps> = ({
template,
onUpdateAgent,
initialMetadata,
agentScriptTimings,
}) => {
const { browser_only, workspace_external_agent } = useFeatureVisibility();
const appSections = organizeAgentApps(agent.apps);
@@ -141,6 +150,12 @@ export const AgentRow: FC<AgentRowProps> = ({
const hasStartupFeatures = Boolean(agent.logs_length);
const healthIssues = getAgentHealthIssues(agent);
const hasAgentIssues = healthIssues.length > 0;
const failedStartTimings = agentScriptTimings?.filter(
(t) =>
t.workspace_agent_id === agent.id &&
t.stage === "start" &&
t.exit_code !== 0,
);
const { proxy } = useProxy();
const [showLogs, setShowLogs] = useState(
(["starting", "start_timeout"].includes(agent.lifecycle_state) ||
@@ -220,7 +235,14 @@ export const AgentRow: FC<AgentRowProps> = ({
agent,
Boolean(hasDevcontainerErrors || shouldShowWildcardWarning),
);
const [selectedLogTab, setSelectedLogTab] = useState("all");
const failedStartupScriptSource = hasAgentIssues
? agent.log_sources.find(
(s) => s.display_name === STARTUP_SCRIPT_DISPLAY_NAME,
)
: undefined;
const [selectedLogTab, setSelectedLogTab] = useState(
failedStartupScriptSource?.id ?? "all",
);
const sourceLogTabs = agent.log_sources
.filter((logSource) => {
// Remove the logSources that have no entries.
@@ -482,13 +504,27 @@ export const AgentRow: FC<AgentRowProps> = ({
<div className={cn("px-4", hasStartupFeatures ? "pb-4" : "py-4")}>
{healthIssues.length > 0 && (
<div className="mb-4 flex flex-col gap-3">
{healthIssues.map((issue) => (
<AgentAlert
key={`${issue.title}-${issue.detail}`}
{...issue}
troubleshootingURL={agent.troubleshooting_url}
/>
))}
{healthIssues.map((issue) => {
const isStartError =
issue.title === agentScriptMessages.start_error.title;
const detail =
isStartError && failedStartTimings?.length ? (
<StartScriptFailureDetail
baseDetail={issue.detail}
timings={failedStartTimings}
/>
) : (
issue.detail
);
return (
<AgentAlert
key={`${issue.title}-${issue.detail}`}
{...issue}
detail={detail}
troubleshootingURL={agent.troubleshooting_url}
/>
);
})}
</div>
)}
{hasStartupFeatures && hasAnyLogs && (