From 59525f879b3a4c29cbfb7cc2ce739f28d2e5aabe Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 26 Aug 2025 12:45:07 -0300 Subject: [PATCH] feat: display startup script logs while agent is starting (#19530) Closes https://github.com/coder/coder/issues/19363 **Screenshot:** Screenshot 2025-08-25 at 11 02 25 **Demo:** https://github.com/user-attachments/assets/07a68e30-b776-44f9-b4ca-e2dd8d124281 --- site/src/pages/TaskPage/TaskPage.stories.tsx | 91 +++++++++---- site/src/pages/TaskPage/TaskPage.tsx | 128 ++++++++++++++----- site/src/pages/TaskPage/TaskTopbar.tsx | 2 +- 3 files changed, 164 insertions(+), 57 deletions(-) diff --git a/site/src/pages/TaskPage/TaskPage.stories.tsx b/site/src/pages/TaskPage/TaskPage.stories.tsx index 6a486442ac..e44fece019 100644 --- a/site/src/pages/TaskPage/TaskPage.stories.tsx +++ b/site/src/pages/TaskPage/TaskPage.stories.tsx @@ -2,17 +2,17 @@ import { MockFailedWorkspace, MockStartingWorkspace, MockStoppedWorkspace, - MockTemplate, MockWorkspace, - MockWorkspaceAgent, + MockWorkspaceAgentLogSource, + MockWorkspaceAgentReady, + MockWorkspaceAgentStarting, MockWorkspaceApp, MockWorkspaceAppStatus, MockWorkspaceResource, mockApiError, } from "testHelpers/entities"; -import { withProxyProvider } from "testHelpers/storybook"; +import { withProxyProvider, withWebSocket } from "testHelpers/storybook"; import type { Meta, StoryObj } from "@storybook/react-vite"; -import { API } from "api/api"; import type { Workspace, WorkspaceApp, @@ -61,28 +61,6 @@ export const WaitingOnBuild: Story = { }, }; -export const WaitingOnBuildWithTemplate: Story = { - beforeEach: () => { - spyOn(API, "getTemplate").mockResolvedValue(MockTemplate); - spyOn(data, "fetchTask").mockResolvedValue({ - prompt: "Create competitors page", - workspace: MockStartingWorkspace, - }); - }, -}; - -export const WaitingOnStatus: Story = { - beforeEach: () => { - spyOn(data, "fetchTask").mockResolvedValue({ - prompt: "Create competitors page", - workspace: { - ...MockWorkspace, - latest_app_status: null, - }, - }); - }, -}; - export const FailedBuild: Story = { beforeEach: () => { spyOn(data, "fetchTask").mockResolvedValue({ @@ -113,6 +91,65 @@ export const TerminatedBuildWithStatus: Story = { }, }; +export const WaitingOnStatus: Story = { + beforeEach: () => { + spyOn(data, "fetchTask").mockResolvedValue({ + prompt: "Create competitors page", + workspace: { + ...MockWorkspace, + latest_app_status: null, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { ...MockWorkspaceResource, agents: [MockWorkspaceAgentReady] }, + ], + }, + }, + }); + }, +}; + +export const WaitingStartupScripts: Story = { + beforeEach: () => { + spyOn(data, "fetchTask").mockResolvedValue({ + prompt: "Create competitors page", + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + has_ai_task: true, + resources: [ + { ...MockWorkspaceResource, agents: [MockWorkspaceAgentStarting] }, + ], + }, + }, + }); + }, + decorators: [withWebSocket], + parameters: { + webSocket: [ + { + event: "message", + data: JSON.stringify( + [ + "\x1b[91mCloning Git repository...", + "\x1b[2;37;41mStarting Docker Daemon...", + "\x1b[1;95mAdding some 🧙magic🧙...", + "Starting VS Code...", + "\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r100 1475 0 1475 0 0 4231 0 --:--:-- --:--:-- --:--:-- 4238", + ].map((line, index) => ({ + id: index, + level: "info", + output: line, + source_id: MockWorkspaceAgentLogSource.id, + created_at: new Date("2024-01-01T12:00:00Z").toISOString(), + })), + ), + }, + ], + }, +}; + export const SidebarAppHealthDisabled: Story = { beforeEach: () => { spyOn(data, "fetchTask").mockResolvedValue({ @@ -223,7 +260,7 @@ const mockResources = ( ...MockWorkspaceResource, agents: [ { - ...MockWorkspaceAgent, + ...MockWorkspaceAgentReady, apps: [ ...(props?.apps ?? []), { diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx index 57f6c81cff..4d84d47fb5 100644 --- a/site/src/pages/TaskPage/TaskPage.tsx +++ b/site/src/pages/TaskPage/TaskPage.tsx @@ -1,7 +1,11 @@ import { API } from "api/api"; import { getErrorDetail, getErrorMessage } from "api/errors"; import { template as templateQueryOptions } from "api/queries/templates"; -import type { Workspace, WorkspaceStatus } from "api/typesGenerated"; +import type { + Workspace, + WorkspaceAgent, + WorkspaceStatus, +} from "api/typesGenerated"; import isChromatic from "chromatic/isChromatic"; import { Button } from "components/Button/Button"; import { Loader } from "components/Loader/Loader"; @@ -9,13 +13,16 @@ import { Margins } from "components/Margins/Margins"; import { ScrollArea } from "components/ScrollArea/ScrollArea"; import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { ArrowLeftIcon, RotateCcwIcon } from "lucide-react"; +import { AgentLogs } from "modules/resources/AgentLogs/AgentLogs"; +import { useAgentLogs } from "modules/resources/useAgentLogs"; import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks"; import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs"; -import { type FC, type ReactNode, useEffect, useRef } from "react"; +import { type FC, type ReactNode, useLayoutEffect, useRef } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { Link as RouterLink, useParams } from "react-router"; +import type { FixedSizeList } from "react-window"; import { pageTitle } from "utils/page"; import { getActiveTransitionStats, @@ -87,6 +94,7 @@ const TaskPage = () => { } let content: ReactNode = null; + const agent = selectAgent(task); if (waitingStatuses.includes(task.workspace.latest_build.status)) { content = ; @@ -132,6 +140,8 @@ const TaskPage = () => { ); + } else if (agent && ["created", "starting"].includes(agent.lifecycle_state)) { + content = ; } else { content = ( @@ -182,7 +192,7 @@ const TaskBuildingWorkspace: FC = ({ task }) => { const scrollAreaRef = useRef(null); // biome-ignore lint/correctness/useExhaustiveDependencies: this effect should run when build logs change - useEffect(() => { + useLayoutEffect(() => { if (isChromatic()) { return; } @@ -196,34 +206,86 @@ const TaskBuildingWorkspace: FC = ({ task }) => { }, [buildLogs]); return ( -
-
-
-

- Starting your workspace -

-
- Your task will be running in a few moments -
-
+
+
+
+
+

+ Starting your workspace +

+

+ Your task will be running in a few moments +

+
-
- - - - + - + + + + +
+
+
+
+ ); +}; + +type TaskStartingAgentProps = { + agent: WorkspaceAgent; +}; + +const TaskStartingAgent: FC = ({ agent }) => { + const logs = useAgentLogs(agent, true); + const listRef = useRef(null); + + useLayoutEffect(() => { + if (listRef.current) { + listRef.current.scrollToItem(logs.length - 1, "end"); + } + }, [logs]); + + return ( +
+
+
+
+

+ Running startup scripts +

+

+ Your task will be running in a few moments +

+
+ +
+
+ ({ + id: l.id, + level: l.level, + output: l.output, + sourceId: l.source_id, + time: l.created_at, + }))} + sources={agent.log_sources} + height={96 * 4} + width="100%" + ref={listRef} + /> +
+
@@ -265,3 +327,11 @@ export const data = { } satisfies Task; }, }; + +function selectAgent(task: Task) { + const agents = task.workspace.latest_build.resources + .flatMap((r) => r.agents) + .filter((a) => !!a); + + return agents.at(0); +} diff --git a/site/src/pages/TaskPage/TaskTopbar.tsx b/site/src/pages/TaskPage/TaskTopbar.tsx index 4f51812b47..945a9fc179 100644 --- a/site/src/pages/TaskPage/TaskTopbar.tsx +++ b/site/src/pages/TaskPage/TaskTopbar.tsx @@ -22,7 +22,7 @@ type TaskTopbarProps = { task: Task }; export const TaskTopbar: FC = ({ task }) => { return ( -
+