diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx
index 7017986c7b686..4a65c6f1be993 100644
--- a/site/src/pages/TaskPage/TaskPage.tsx
+++ b/site/src/pages/TaskPage/TaskPage.tsx
@@ -2,25 +2,28 @@ 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 isChromatic from "chromatic/isChromatic";
import { Button } from "components/Button/Button";
import { Loader } from "components/Loader/Loader";
import { Margins } from "components/Margins/Margins";
+import { ScrollArea } from "components/ScrollArea/ScrollArea";
import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs";
import { ArrowLeftIcon, RotateCcwIcon } from "lucide-react";
import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks";
-import type { ReactNode } from "react";
+import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
+import { type FC, type ReactNode, useEffect, 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 { ellipsizeText } from "utils/ellipsizeText";
import { pageTitle } from "utils/page";
import {
- ActiveTransition,
+ getActiveTransitionStats,
WorkspaceBuildProgress,
} from "../WorkspacePage/WorkspaceBuildProgress";
import { TaskApps } from "./TaskApps";
import { TaskSidebar } from "./TaskSidebar";
+import { TaskTopbar } from "./TaskTopbar";
const TaskPage = () => {
const { workspace: workspaceName, username } = useParams() as {
@@ -37,18 +40,7 @@ const TaskPage = () => {
refetchInterval: 5_000,
});
- const { data: template } = useQuery({
- ...templateQueryOptions(task?.workspace.template_id ?? ""),
- enabled: Boolean(task),
- });
-
const waitingStatuses: WorkspaceStatus[] = ["starting", "pending"];
- const shouldStreamBuildLogs =
- task && waitingStatuses.includes(task.workspace.latest_build.status);
- const buildLogs = useWorkspaceBuildLogs(
- task?.workspace.latest_build.id ?? "",
- shouldStreamBuildLogs,
- );
if (error) {
return (
@@ -95,38 +87,9 @@ const TaskPage = () => {
}
let content: ReactNode = null;
- const _terminatedStatuses: WorkspaceStatus[] = [
- "canceled",
- "canceling",
- "deleted",
- "deleting",
- "stopped",
- "stopping",
- ];
if (waitingStatuses.includes(task.workspace.latest_build.status)) {
- // If no template yet, use an indeterminate progress bar.
- const transition = (template &&
- ActiveTransition(template, task.workspace)) || { P50: 0, P95: null };
- const lastStage =
- buildLogs?.[buildLogs.length - 1]?.stage || "Waiting for build status";
- content = (
-
-
-
- Starting your workspace
-
-
{lastStage}
-
-
-
-
-
- );
+ content = ;
} else if (task.workspace.latest_build.status === "failed") {
content = (
@@ -170,14 +133,7 @@ const TaskPage = () => {
);
} else {
- content =
;
- }
-
- return (
- <>
-
- {pageTitle(ellipsizeText(task.prompt, 64) ?? "Task")}
-
+ content = (
@@ -185,14 +141,95 @@ const TaskPage = () => {
- {content}
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+ {pageTitle(ellipsizeText(task.prompt, 64))}
+
+
+
+
+ {content}
+
>
);
};
export default TaskPage;
+type TaskBuildingWorkspaceProps = { task: Task };
+
+const TaskBuildingWorkspace: FC
= ({ task }) => {
+ const { data: template } = useQuery(
+ templateQueryOptions(task.workspace.template_id),
+ );
+
+ const buildLogs = useWorkspaceBuildLogs(task?.workspace.latest_build.id);
+
+ // If no template yet, use an indeterminate progress bar.
+ const transitionStats = (template &&
+ getActiveTransitionStats(template, task.workspace)) || {
+ P50: 0,
+ P95: null,
+ };
+
+ const scrollAreaRef = useRef(null);
+ // biome-ignore lint/correctness/useExhaustiveDependencies: this effect should run when build logs change
+ useEffect(() => {
+ if (isChromatic()) {
+ return;
+ }
+ const scrollAreaEl = scrollAreaRef.current;
+ const scrollAreaViewportEl = scrollAreaEl?.querySelector(
+ "[data-radix-scroll-area-viewport]",
+ );
+ if (scrollAreaViewportEl) {
+ scrollAreaViewportEl.scrollTop = scrollAreaViewportEl.scrollHeight;
+ }
+ }, [buildLogs]);
+
+ return (
+
+ );
+};
+
export class WorkspaceDoesNotHaveAITaskError extends Error {
constructor(workspace: Workspace) {
super(
@@ -228,3 +265,7 @@ export const data = {
} satisfies Task;
},
};
+
+const ellipsizeText = (text: string, maxLength = 80): string => {
+ return text.length <= maxLength ? text : `${text.slice(0, maxLength - 3)}...`;
+};
diff --git a/site/src/pages/TaskPage/TaskSidebar.tsx b/site/src/pages/TaskPage/TaskSidebar.tsx
index 2309884d166b8..eb1aeb6d59375 100644
--- a/site/src/pages/TaskPage/TaskSidebar.tsx
+++ b/site/src/pages/TaskPage/TaskSidebar.tsx
@@ -1,24 +1,8 @@
import type { WorkspaceApp } from "api/typesGenerated";
-import { Button } from "components/Button/Button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "components/DropdownMenu/DropdownMenu";
import { Spinner } from "components/Spinner/Spinner";
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "components/Tooltip/Tooltip";
-import { ArrowLeftIcon, EllipsisVerticalIcon } from "lucide-react";
import type { Task } from "modules/tasks/tasks";
import type { FC } from "react";
-import { Link as RouterLink } from "react-router";
import { TaskAppIFrame } from "./TaskAppIframe";
-import { TaskStatusLink } from "./TaskStatusLink";
type TaskSidebarProps = {
task: Task;
@@ -84,60 +68,6 @@ export const TaskSidebar: FC = ({ task }) => {
return (