diff --git a/site/src/pages/TaskPage/TaskPage.stories.tsx b/site/src/pages/TaskPage/TaskPage.stories.tsx index 6a486442ace8c..e44fece019f7b 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,56 +61,93 @@ export const WaitingOnBuild: Story = { }, }; -export const WaitingOnBuildWithTemplate: Story = { +export const FailedBuild: Story = { beforeEach: () => { - spyOn(API, "getTemplate").mockResolvedValue(MockTemplate); spyOn(data, "fetchTask").mockResolvedValue({ prompt: "Create competitors page", - workspace: MockStartingWorkspace, + workspace: MockFailedWorkspace, }); }, }; -export const WaitingOnStatus: Story = { +export const TerminatedBuild: Story = { beforeEach: () => { spyOn(data, "fetchTask").mockResolvedValue({ prompt: "Create competitors page", - workspace: { - ...MockWorkspace, - latest_app_status: null, - }, + workspace: MockStoppedWorkspace, }); }, }; -export const FailedBuild: Story = { +export const TerminatedBuildWithStatus: Story = { beforeEach: () => { spyOn(data, "fetchTask").mockResolvedValue({ prompt: "Create competitors page", - workspace: MockFailedWorkspace, + workspace: { + ...MockStoppedWorkspace, + latest_app_status: MockWorkspaceAppStatus, + }, }); }, }; -export const TerminatedBuild: Story = { +export const WaitingOnStatus: Story = { beforeEach: () => { spyOn(data, "fetchTask").mockResolvedValue({ prompt: "Create competitors page", - workspace: MockStoppedWorkspace, + workspace: { + ...MockWorkspace, + latest_app_status: null, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { ...MockWorkspaceResource, agents: [MockWorkspaceAgentReady] }, + ], + }, + }, }); }, }; -export const TerminatedBuildWithStatus: Story = { +export const WaitingStartupScripts: Story = { beforeEach: () => { spyOn(data, "fetchTask").mockResolvedValue({ prompt: "Create competitors page", workspace: { - ...MockStoppedWorkspace, - latest_app_status: MockWorkspaceAppStatus, + ...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 = { @@ -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 57f6c81cff277..4d84d47fb5ff7 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 4f51812b4712d..945a9fc179537 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 ( -
+