From 2be4511c7defcb5c4516117e1a5eb4a9d98e984d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 18 Nov 2025 13:25:15 +0000 Subject: [PATCH 1/5] feat(site): add startup script error alerts to Task Page Add warning alerts to the Task Page when workspace startup scripts fail or timeout. The alerts display inline above the log viewer to maintain debugging context while providing helpful documentation links. - Create TaskStartupAlert component with error and timeout variants - Integrate alert into TaskStartingAgent component - Extend TaskPage logic to render TaskStartingAgent for error states - Add Storybook stories for component and full page integration - Follow existing pattern from TerminalAlerts component The alerts are dismissible and link to troubleshooting documentation: - /admin/templates/troubleshooting#startup-script-exited-with-an-error - /admin/templates/troubleshooting#startup-script-issues - /admin/templates/troubleshooting#your-workspace-may-be-incomplete --- site/src/pages/TaskPage/TaskPage.stories.tsx | 107 ++++++++++++++++++ site/src/pages/TaskPage/TaskPage.tsx | 9 +- .../TaskPage/TaskStartupAlert.stories.tsx | 36 ++++++ site/src/pages/TaskPage/TaskStartupAlert.tsx | 101 +++++++++++++++++ 4 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 site/src/pages/TaskPage/TaskStartupAlert.stories.tsx create mode 100644 site/src/pages/TaskPage/TaskStartupAlert.tsx diff --git a/site/src/pages/TaskPage/TaskPage.stories.tsx b/site/src/pages/TaskPage/TaskPage.stories.tsx index b247a2a16b377..9a048b41c7306 100644 --- a/site/src/pages/TaskPage/TaskPage.stories.tsx +++ b/site/src/pages/TaskPage/TaskPage.stories.tsx @@ -9,7 +9,9 @@ import { MockWorkspace, MockWorkspaceAgentLogSource, MockWorkspaceAgentReady, + MockWorkspaceAgentStartError, MockWorkspaceAgentStarting, + MockWorkspaceAgentStartTimeout, MockWorkspaceApp, MockWorkspaceAppStatus, MockWorkspaceResource, @@ -218,6 +220,111 @@ export const WaitingStartupScripts: Story = { }, }; +export const StartupScriptError: Story = { + decorators: [withWebSocket], + parameters: { + queries: [ + { + key: ["tasks", MockTask.owner_name, MockTask.id], + data: MockTask, + }, + { + key: [ + "workspace", + MockTask.owner_name, + MockTask.workspace_name, + "settings", + ], + data: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + has_ai_task: true, + resources: [ + { + ...MockWorkspaceResource, + agents: [MockWorkspaceAgentStartError], + }, + ], + }, + }, + }, + ], + webSocket: [ + { + event: "message", + data: JSON.stringify( + [ + "Cloning Git repository...", + "Starting application...", + "\x1b[91mError: Failed to connect to database", + "\x1b[91mStartup script exited with code 1", + ].map((line, index) => ({ + id: index, + level: index >= 2 ? "error" : "info", + output: line, + source_id: MockWorkspaceAgentLogSource.id, + created_at: new Date("2024-01-01T12:00:00Z").toISOString(), + })), + ), + }, + ], + }, +}; + +export const StartupScriptTimeout: Story = { + decorators: [withWebSocket], + parameters: { + queries: [ + { + key: ["tasks", MockTask.owner_name, MockTask.id], + data: MockTask, + }, + { + key: [ + "workspace", + MockTask.owner_name, + MockTask.workspace_name, + "settings", + ], + data: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + has_ai_task: true, + resources: [ + { + ...MockWorkspaceResource, + agents: [MockWorkspaceAgentStartTimeout], + }, + ], + }, + }, + }, + ], + webSocket: [ + { + event: "message", + data: JSON.stringify( + [ + "Cloning Git repository...", + "Starting application...", + "Waiting for dependencies...", + "Still waiting...", + "\x1b[93mWarning: Startup script exceeded timeout limit", + ].map((line, index) => ({ + id: index, + level: index === 4 ? "warn" : "info", + output: line, + source_id: MockWorkspaceAgentLogSource.id, + created_at: new Date("2024-01-01T12:00:00Z").toISOString(), + })), + ), + }, + ], + }, +}; + export const SidebarAppNotFound: Story = { beforeEach: () => { const [task, workspace] = mockTaskWithWorkspace( diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx index f57d0e9f1772c..672a2f9025541 100644 --- a/site/src/pages/TaskPage/TaskPage.tsx +++ b/site/src/pages/TaskPage/TaskPage.tsx @@ -44,6 +44,7 @@ import { } from "../WorkspacePage/WorkspaceBuildProgress"; import { TaskAppIFrame } from "./TaskAppIframe"; import { TaskApps } from "./TaskApps"; +import { TaskStartupAlert } from "./TaskStartupAlert"; import { TaskTopbar } from "./TaskTopbar"; const TaskPageLayout: FC = ({ children }) => { @@ -145,7 +146,12 @@ const TaskPage = () => { ); } else if (workspace.latest_build.status !== "running") { content = ; - } else if (agent && ["created", "starting"].includes(agent.lifecycle_state)) { + } else if ( + agent && + ["created", "starting", "start_error", "start_timeout"].includes( + agent.lifecycle_state, + ) + ) { content = ; } else { const chatApp = getAllAppsWithAgent(workspace).find( @@ -378,6 +384,7 @@ const TaskStartingAgent: FC = ({ agent }) => {
+
= { + title: "pages/TaskPage/TaskStartupAlert", + component: TaskStartupAlert, + parameters: { + layout: "padded", + }, +}; + +export default meta; +type Story = StoryObj; + +export const StartError: Story = { + args: { + agent: MockWorkspaceAgentStartError, + }, +}; + +export const StartTimeout: Story = { + args: { + agent: MockWorkspaceAgentStartTimeout, + }, +}; + +export const NoAlert: Story = { + args: { + agent: MockWorkspaceAgentReady, + }, +}; diff --git a/site/src/pages/TaskPage/TaskStartupAlert.tsx b/site/src/pages/TaskPage/TaskStartupAlert.tsx new file mode 100644 index 0000000000000..56e26f082e539 --- /dev/null +++ b/site/src/pages/TaskPage/TaskStartupAlert.tsx @@ -0,0 +1,101 @@ +import Link from "@mui/material/Link"; +import type { WorkspaceAgent } from "api/typesGenerated"; +import { Alert } from "components/Alert/Alert"; +import type { FC } from "react"; +import { docs } from "utils/docs"; + +type TaskStartupAlertProps = { + agent: WorkspaceAgent; +}; + +export const TaskStartupAlert: FC = ({ agent }) => { + const lifecycleState = agent.lifecycle_state; + + if (lifecycleState === "start_error") { + return ; + } + + if (lifecycleState === "start_timeout") { + return ; + } + + return null; +}; + +const ErrorScriptAlert: FC = () => { + return ( + + The workspace{" "} + + startup script has exited with an error + + . We recommend{" "} + + debugging the startup script + {" "} + because{" "} + + your workspace may be incomplete + + . + + ); +}; + +const TimeoutScriptAlert: FC = () => { + return ( + + The workspace{" "} + + startup script has timed out + + . We recommend{" "} + + debugging the startup script + {" "} + because{" "} + + your workspace may be incomplete + + . + + ); +}; From 9c8af27c39812f3d6f656a98192ac01083e11639 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 18 Nov 2025 14:32:55 +0000 Subject: [PATCH 2/5] fix(site): use sentence case for alert link titles and change to 'A workspace' - Update all title attributes to use sentence case (capitalize first word only) - Change 'The workspace' to 'A workspace' in alert messages --- site/src/pages/TaskPage/TaskStartupAlert.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/site/src/pages/TaskPage/TaskStartupAlert.tsx b/site/src/pages/TaskPage/TaskStartupAlert.tsx index 56e26f082e539..13d586dc8770d 100644 --- a/site/src/pages/TaskPage/TaskStartupAlert.tsx +++ b/site/src/pages/TaskPage/TaskStartupAlert.tsx @@ -25,9 +25,9 @@ export const TaskStartupAlert: FC = ({ agent }) => { const ErrorScriptAlert: FC = () => { return ( - The workspace{" "} + A workspace{" "} { . We recommend{" "} { {" "} because{" "} { const TimeoutScriptAlert: FC = () => { return ( - The workspace{" "} + A workspace{" "} { . We recommend{" "} { {" "} because{" "} Date: Tue, 18 Nov 2025 16:25:38 +0000 Subject: [PATCH 3/5] feat(site): refactor startup alerts to compact warning button in topbar - Convert TaskStartupAlert from blocking banner to warning button with tooltip - Move button to topbar, positioned left of Prompt button - Change from agent-based to task.workspace_agent_lifecycle property - Remove start_error and start_timeout from blocking states - Extract shared StartupWarningButtonBase component to eliminate duplication - Update Storybook stories for new button-based UI - Users can now access full task interface during startup errors This improves UX by making startup warnings non-intrusive while still visible and accessible via hover tooltip with documentation links. --- site/src/pages/TaskPage/TaskPage.stories.tsx | 17 +- site/src/pages/TaskPage/TaskPage.tsx | 9 +- .../TaskPage/TaskStartupAlert.stories.tsx | 42 +++-- site/src/pages/TaskPage/TaskStartupAlert.tsx | 171 ++++++++++-------- site/src/pages/TaskPage/TaskTopbar.tsx | 3 + 5 files changed, 136 insertions(+), 106 deletions(-) diff --git a/site/src/pages/TaskPage/TaskPage.stories.tsx b/site/src/pages/TaskPage/TaskPage.stories.tsx index 9a048b41c7306..27f0b60e3a16f 100644 --- a/site/src/pages/TaskPage/TaskPage.stories.tsx +++ b/site/src/pages/TaskPage/TaskPage.stories.tsx @@ -7,11 +7,10 @@ import { MockTasks, MockUserOwner, MockWorkspace, + MockWorkspaceAgent, MockWorkspaceAgentLogSource, MockWorkspaceAgentReady, - MockWorkspaceAgentStartError, MockWorkspaceAgentStarting, - MockWorkspaceAgentStartTimeout, MockWorkspaceApp, MockWorkspaceAppStatus, MockWorkspaceResource, @@ -226,7 +225,10 @@ export const StartupScriptError: Story = { queries: [ { key: ["tasks", MockTask.owner_name, MockTask.id], - data: MockTask, + data: { + ...MockTask, + workspace_agent_lifecycle: "start_error", + }, }, { key: [ @@ -243,7 +245,7 @@ export const StartupScriptError: Story = { resources: [ { ...MockWorkspaceResource, - agents: [MockWorkspaceAgentStartError], + agents: [MockWorkspaceAgent], }, ], }, @@ -278,7 +280,10 @@ export const StartupScriptTimeout: Story = { queries: [ { key: ["tasks", MockTask.owner_name, MockTask.id], - data: MockTask, + data: { + ...MockTask, + workspace_agent_lifecycle: "start_timeout", + }, }, { key: [ @@ -295,7 +300,7 @@ export const StartupScriptTimeout: Story = { resources: [ { ...MockWorkspaceResource, - agents: [MockWorkspaceAgentStartTimeout], + agents: [MockWorkspaceAgent], }, ], }, diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx index 672a2f9025541..f57d0e9f1772c 100644 --- a/site/src/pages/TaskPage/TaskPage.tsx +++ b/site/src/pages/TaskPage/TaskPage.tsx @@ -44,7 +44,6 @@ import { } from "../WorkspacePage/WorkspaceBuildProgress"; import { TaskAppIFrame } from "./TaskAppIframe"; import { TaskApps } from "./TaskApps"; -import { TaskStartupAlert } from "./TaskStartupAlert"; import { TaskTopbar } from "./TaskTopbar"; const TaskPageLayout: FC = ({ children }) => { @@ -146,12 +145,7 @@ const TaskPage = () => { ); } else if (workspace.latest_build.status !== "running") { content = ; - } else if ( - agent && - ["created", "starting", "start_error", "start_timeout"].includes( - agent.lifecycle_state, - ) - ) { + } else if (agent && ["created", "starting"].includes(agent.lifecycle_state)) { content = ; } else { const chatApp = getAllAppsWithAgent(workspace).find( @@ -384,7 +378,6 @@ const TaskStartingAgent: FC = ({ agent }) => {
-
= { - title: "pages/TaskPage/TaskStartupAlert", - component: TaskStartupAlert, +const meta: Meta = { + title: "pages/TaskPage/TaskStartupWarningButton", + component: TaskStartupWarningButton, parameters: { layout: "padded", }, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const StartError: Story = { args: { - agent: MockWorkspaceAgentStartError, + task: { + ...MockTask, + workspace_agent_lifecycle: "start_error", + }, }, }; export const StartTimeout: Story = { args: { - agent: MockWorkspaceAgentStartTimeout, + task: { + ...MockTask, + workspace_agent_lifecycle: "start_timeout", + }, }, }; -export const NoAlert: Story = { +export const NoWarning: Story = { args: { - agent: MockWorkspaceAgentReady, + task: { + ...MockTask, + workspace_agent_lifecycle: "ready", + }, + }, +}; + +export const NullLifecycle: Story = { + args: { + task: { + ...MockTask, + workspace_agent_lifecycle: null, + }, }, }; diff --git a/site/src/pages/TaskPage/TaskStartupAlert.tsx b/site/src/pages/TaskPage/TaskStartupAlert.tsx index 13d586dc8770d..069467c115846 100644 --- a/site/src/pages/TaskPage/TaskStartupAlert.tsx +++ b/site/src/pages/TaskPage/TaskStartupAlert.tsx @@ -1,101 +1,116 @@ import Link from "@mui/material/Link"; -import type { WorkspaceAgent } from "api/typesGenerated"; -import { Alert } from "components/Alert/Alert"; +import type { Task } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; +import { TriangleAlertIcon } from "lucide-react"; import type { FC } from "react"; import { docs } from "utils/docs"; -type TaskStartupAlertProps = { - agent: WorkspaceAgent; +type TaskStartupWarningButtonProps = { + task: Task; }; -export const TaskStartupAlert: FC = ({ agent }) => { - const lifecycleState = agent.lifecycle_state; +export const TaskStartupWarningButton: FC = ({ + task, +}) => { + const lifecycleState = task.workspace_agent_lifecycle; + + if (!lifecycleState) { + return null; + } if (lifecycleState === "start_error") { - return ; + return ; } if (lifecycleState === "start_timeout") { - return ; + return ; } return null; }; -const ErrorScriptAlert: FC = () => { +type StartupWarningButtonBaseProps = { + label: string; + errorMessage: string; +}; + +const StartupWarningButtonBase: FC = ({ + label, + errorMessage, +}) => { + return ( + + + + + + +

+ A workspace{" "} + + {errorMessage} + + . We recommend{" "} + + debugging the startup script + {" "} + because{" "} + + your workspace may be incomplete + + . +

+
+
+
+ ); +}; + +const ErrorScriptButton: FC = () => { return ( - - A workspace{" "} - - startup script has exited with an error - - . We recommend{" "} - - debugging the startup script - {" "} - because{" "} - - your workspace may be incomplete - - . - + ); }; -const TimeoutScriptAlert: FC = () => { +const TimeoutScriptButton: FC = () => { return ( - - A workspace{" "} - - startup script has timed out - - . We recommend{" "} - - debugging the startup script - {" "} - because{" "} - - your workspace may be incomplete - - . - + ); }; diff --git a/site/src/pages/TaskPage/TaskTopbar.tsx b/site/src/pages/TaskPage/TaskTopbar.tsx index 3d22631ae14b8..9313ee38fc83a 100644 --- a/site/src/pages/TaskPage/TaskTopbar.tsx +++ b/site/src/pages/TaskPage/TaskTopbar.tsx @@ -16,6 +16,7 @@ import { } from "lucide-react"; import type { FC } from "react"; import { Link as RouterLink } from "react-router"; +import { TaskStartupWarningButton } from "./TaskStartupAlert"; import { TaskStatusLink } from "./TaskStatusLink"; type TaskTopbarProps = { task: Task; workspace: Workspace }; @@ -46,6 +47,8 @@ export const TaskTopbar: FC = ({ task, workspace }) => { )}
+ + From 1be03bf0394cd7a376846610ec9dee06eca4936c Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 19 Nov 2025 11:10:56 +0000 Subject: [PATCH 4/5] refactor(site): improve TaskStartupWarningButton API and file naming - Renamed TaskStartupAlert.tsx to TaskStartupWarningButton.tsx to match exported component name - Changed prop from 'task: Task' to 'lifecycleState: WorkspaceAgentLifecycle | null' for cleaner API - Refactored conditional logic to use switch statement instead of if statements - Updated TaskTopbar.tsx to pass lifecycleState directly - Updated all Storybook stories to use new lifecycleState prop - Removed unnecessary MockTask dependency from stories --- ...x => TaskStartupWarningButton.stories.tsx} | 23 ++++--------------- ...Alert.tsx => TaskStartupWarningButton.tsx} | 23 ++++++++----------- site/src/pages/TaskPage/TaskTopbar.tsx | 6 +++-- 3 files changed, 19 insertions(+), 33 deletions(-) rename site/src/pages/TaskPage/{TaskStartupAlert.stories.tsx => TaskStartupWarningButton.stories.tsx} (57%) rename site/src/pages/TaskPage/{TaskStartupAlert.tsx => TaskStartupWarningButton.tsx} (88%) diff --git a/site/src/pages/TaskPage/TaskStartupAlert.stories.tsx b/site/src/pages/TaskPage/TaskStartupWarningButton.stories.tsx similarity index 57% rename from site/src/pages/TaskPage/TaskStartupAlert.stories.tsx rename to site/src/pages/TaskPage/TaskStartupWarningButton.stories.tsx index a4299f975c80b..482fa396016d7 100644 --- a/site/src/pages/TaskPage/TaskStartupAlert.stories.tsx +++ b/site/src/pages/TaskPage/TaskStartupWarningButton.stories.tsx @@ -1,6 +1,5 @@ -import { MockTask } from "testHelpers/entities"; import type { Meta, StoryObj } from "@storybook/react-vite"; -import { TaskStartupWarningButton } from "./TaskStartupAlert"; +import { TaskStartupWarningButton } from "./TaskStartupWarningButton"; const meta: Meta = { title: "pages/TaskPage/TaskStartupWarningButton", @@ -15,36 +14,24 @@ type Story = StoryObj; export const StartError: Story = { args: { - task: { - ...MockTask, - workspace_agent_lifecycle: "start_error", - }, + lifecycleState: "start_error", }, }; export const StartTimeout: Story = { args: { - task: { - ...MockTask, - workspace_agent_lifecycle: "start_timeout", - }, + lifecycleState: "start_timeout", }, }; export const NoWarning: Story = { args: { - task: { - ...MockTask, - workspace_agent_lifecycle: "ready", - }, + lifecycleState: "ready", }, }; export const NullLifecycle: Story = { args: { - task: { - ...MockTask, - workspace_agent_lifecycle: null, - }, + lifecycleState: null, }, }; diff --git a/site/src/pages/TaskPage/TaskStartupAlert.tsx b/site/src/pages/TaskPage/TaskStartupWarningButton.tsx similarity index 88% rename from site/src/pages/TaskPage/TaskStartupAlert.tsx rename to site/src/pages/TaskPage/TaskStartupWarningButton.tsx index 069467c115846..3791f3a1cac60 100644 --- a/site/src/pages/TaskPage/TaskStartupAlert.tsx +++ b/site/src/pages/TaskPage/TaskStartupWarningButton.tsx @@ -1,5 +1,5 @@ import Link from "@mui/material/Link"; -import type { Task } from "api/typesGenerated"; +import type { WorkspaceAgentLifecycle } from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { Tooltip, @@ -12,27 +12,24 @@ import type { FC } from "react"; import { docs } from "utils/docs"; type TaskStartupWarningButtonProps = { - task: Task; + lifecycleState: WorkspaceAgentLifecycle | null; }; export const TaskStartupWarningButton: FC = ({ - task, + lifecycleState, }) => { - const lifecycleState = task.workspace_agent_lifecycle; - if (!lifecycleState) { return null; } - if (lifecycleState === "start_error") { - return ; - } - - if (lifecycleState === "start_timeout") { - return ; + switch (lifecycleState) { + case "start_error": + return ; + case "start_timeout": + return ; + default: + return null; } - - return null; }; type StartupWarningButtonBaseProps = { diff --git a/site/src/pages/TaskPage/TaskTopbar.tsx b/site/src/pages/TaskPage/TaskTopbar.tsx index 9313ee38fc83a..b5affcbfffe0b 100644 --- a/site/src/pages/TaskPage/TaskTopbar.tsx +++ b/site/src/pages/TaskPage/TaskTopbar.tsx @@ -16,7 +16,7 @@ import { } from "lucide-react"; import type { FC } from "react"; import { Link as RouterLink } from "react-router"; -import { TaskStartupWarningButton } from "./TaskStartupAlert"; +import { TaskStartupWarningButton } from "./TaskStartupWarningButton"; import { TaskStatusLink } from "./TaskStatusLink"; type TaskTopbarProps = { task: Task; workspace: Workspace }; @@ -47,7 +47,9 @@ export const TaskTopbar: FC = ({ task, workspace }) => { )}
- + From 8a8b9665dc930fb1fb3dd73e5da9072ae176516e Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 19 Nov 2025 11:18:15 +0000 Subject: [PATCH 5/5] refactor(site): improve TaskStartupWarningButton API - Make lifecycleState parameter optional - Remove redundant early null guard (handled by switch default) - Replace MUI Link with our own Link component for consistent styling and external icon --- site/src/pages/TaskPage/TaskStartupWarningButton.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/site/src/pages/TaskPage/TaskStartupWarningButton.tsx b/site/src/pages/TaskPage/TaskStartupWarningButton.tsx index 3791f3a1cac60..fab1b1a9b32e6 100644 --- a/site/src/pages/TaskPage/TaskStartupWarningButton.tsx +++ b/site/src/pages/TaskPage/TaskStartupWarningButton.tsx @@ -1,6 +1,6 @@ -import Link from "@mui/material/Link"; import type { WorkspaceAgentLifecycle } from "api/typesGenerated"; import { Button } from "components/Button/Button"; +import { Link } from "components/Link/Link"; import { Tooltip, TooltipContent, @@ -12,16 +12,12 @@ import type { FC } from "react"; import { docs } from "utils/docs"; type TaskStartupWarningButtonProps = { - lifecycleState: WorkspaceAgentLifecycle | null; + lifecycleState?: WorkspaceAgentLifecycle | null; }; export const TaskStartupWarningButton: FC = ({ lifecycleState, }) => { - if (!lifecycleState) { - return null; - } - switch (lifecycleState) { case "start_error": return ;