Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: show workspace build logs during tasks creation#19413

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
BrunoQuaresma merged 5 commits intomainfrombq/19363
Aug 21, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 94 additions & 53 deletionssite/src/pages/TaskPage/TaskPage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand All@@ -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 (
Expand DownExpand Up@@ -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 = (
<div className="w-full min-h-80 flex flex-col">
<div className="flex flex-col items-center grow justify-center">
<h3 className="m-0 font-medium text-content-primary text-base">
Starting your workspace
</h3>
<div className="text-content-secondary text-sm">{lastStage}</div>
</div>
<div className="w-full">
<WorkspaceBuildProgress
workspace={task.workspace}
transitionStats={transition}
variant="task"
/>
</div>
</div>
);
content = <TaskBuildingWorkspace task={task} />;
} else if (task.workspace.latest_build.status === "failed") {
content = (
<div className="w-full min-h-80 flex items-center justify-center">
Expand DownExpand Up@@ -170,29 +133,103 @@ const TaskPage = () => {
</Margins>
);
} else {
content = <TaskApps task={task} />;
}

return (
<>
<Helmet>
<title>{pageTitle(ellipsizeText(task.prompt, 64) ?? "Task")}</title>
</Helmet>
content = (
<PanelGroup autoSaveId="task" direction="horizontal">
<Panel defaultSize={25} minSize={20}>
<TaskSidebar task={task} />
</Panel>
<PanelResizeHandle>
<div className="w-1 bg-border h-full hover:bg-border-hover transition-all relative" />
</PanelResizeHandle>
<Panel className="[&>*]:h-full">{content}</Panel>
<Panel className="[&>*]:h-full">
<TaskApps task={task} />
</Panel>
</PanelGroup>
);
}

return (
<>
<Helmet>
<title>{pageTitle(ellipsizeText(task.prompt, 64))}</title>
</Helmet>

<div className="flex flex-col h-full">
<TaskTopbar task={task} />
{content}
</div>
</>
);
};

export default TaskPage;

type TaskBuildingWorkspaceProps = { task: Task };

const TaskBuildingWorkspace: FC<TaskBuildingWorkspaceProps> = ({ 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<HTMLDivElement>(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<HTMLDivElement>(
"[data-radix-scroll-area-viewport]",
);
if (scrollAreaViewportEl) {
scrollAreaViewportEl.scrollTop = scrollAreaViewportEl.scrollHeight;
}
}, [buildLogs]);

return (
<section className="w-full h-full flex justify-center items-center p-6 overflow-y-auto">
<div className="flex flex-col gap-6 items-center w-full">
<header className="flex flex-col items-center text-center">
<h3 className="m-0 font-medium text-content-primary text-xl">
Starting your workspace
</h3>
<div className="text-content-secondary">
Your task will be running in a few moments
</div>
</header>

<div className="w-full max-w-screen-lg flex flex-col gap-4 overflow-hidden">
<WorkspaceBuildProgress
workspace={task.workspace}
transitionStats={transitionStats}
variant="task"
/>

<ScrollArea
ref={scrollAreaRef}
className="h-96 border border-solid border-border rounded-lg"
>
<WorkspaceBuildLogs
sticky
className="border-0 rounded-none"
logs={buildLogs ?? []}
/>
</ScrollArea>
</div>
</div>
</section>
);
};

export class WorkspaceDoesNotHaveAITaskError extends Error {
constructor(workspace: Workspace) {
super(
Expand DownExpand Up@@ -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)}...`;
};
70 changes: 0 additions & 70 deletionssite/src/pages/TaskPage/TaskSidebar.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -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;
Expand DownExpand Up@@ -84,60 +68,6 @@ export const TaskSidebar: FC<TaskSidebarProps> = ({ task }) => {

return (
<aside className="flex flex-col h-full shrink-0 w-full">
<header className="border-0 border-b border-solid border-border p-4 pt-0">
<div className="flex items-center justify-between py-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="subtle" asChild className="-ml-2">
<RouterLink to="/tasks">
<ArrowLeftIcon />
<span className="sr-only">Back to tasks</span>
</RouterLink>
</Button>
</TooltipTrigger>
<TooltipContent>Back to tasks</TooltipContent>
</Tooltip>
</TooltipProvider>

<DropdownMenu>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="subtle" className="-mr-2">
<EllipsisVerticalIcon />
<span className="sr-only">Settings</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Settings</TooltipContent>
</Tooltip>
</TooltipProvider>

<DropdownMenuContent>
<DropdownMenuItem asChild>
<RouterLink
to={`/@${task.workspace.owner_name}/${task.workspace.name}`}
>
View workspace
</RouterLink>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>

<h1 className="m-0 mt-1 text-base font-medium truncate">
{task.prompt || task.workspace.name}
</h1>

{task.workspace.latest_app_status?.uri && (
<div className="flex items-center gap-2 mt-2 flex-wrap">
<TaskStatusLink uri={task.workspace.latest_app_status.uri} />
</div>
)}
</header>

{sidebarAppStatus === "healthy" && sidebarApp ? (
<TaskAppIFrame
active
Expand Down
50 changes: 50 additions & 0 deletionssite/src/pages/TaskPage/TaskTopbar.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
import { Button } from "components/Button/Button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { ArrowLeftIcon } from "lucide-react";
import type { Task } from "modules/tasks/tasks";
import type { FC } from "react";
import { Link as RouterLink } from "react-router";
import { TaskStatusLink } from "./TaskStatusLink";

type TaskTopbarProps = { task: Task };

export const TaskTopbar: FC<TaskTopbarProps> = ({ task }) => {
return (
<header className="flex items-center px-3 h-14 border-solid border-border border-0 border-b">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="subtle" asChild>
<RouterLink to="/tasks">
<ArrowLeftIcon />
<span className="sr-only">Back to tasks</span>
</RouterLink>
</Button>
</TooltipTrigger>
<TooltipContent>Back to tasks</TooltipContent>
</Tooltip>
</TooltipProvider>

<h1 className="m-0 text-base font-medium truncate">{task.prompt}</h1>

{task.workspace.latest_app_status?.uri && (
<div className="flex items-center gap-2 flex-wrap ml-4">
<TaskStatusLink uri={task.workspace.latest_app_status.uri} />
</div>
)}

<Button asChild size="sm" variant="outline" className="ml-auto">
<RouterLink
to={`/@${task.workspace.owner_name}/${task.workspace.name}`}
>
View workspace
</RouterLink>
</Button>
</header>
);
};
6 changes: 4 additions & 2 deletionssite/src/pages/WorkspacePage/Workspace.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ import { ResourcesSidebar } from "./ResourcesSidebar";
import { resourceOptionValue, useResourcesNav } from "./useResourcesNav";
import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection";
import {
ActiveTransition,
getActiveTransitionStats,
WorkspaceBuildProgress,
} from "./WorkspaceBuildProgress";
import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner";
Expand DownExpand Up@@ -68,7 +68,9 @@ export const Workspace: FC<WorkspaceProps> = ({
const navigate = useNavigate();

const transitionStats =
template !== undefined ? ActiveTransition(template, workspace) : undefined;
template !== undefined
? getActiveTransitionStats(template, workspace)
: undefined;

const sidebarOption = useSearchParamsKey({ key: "sidebar" });
const setSidebarOption = (newOption: string) => {
Expand Down
4 changes: 2 additions & 2 deletionssite/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,9 +9,9 @@ import { type FC, useEffect, useState } from "react";

dayjs.extend(duration);

//ActiveTransition gets the build estimate for the workspace,
//getActiveTransitionStats gets the build estimate for the workspace,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

ty for the rename

BrunoQuaresma reacted with heart emoji
// if it is in a transition state.
export constActiveTransition = (
export constgetActiveTransitionStats = (
template: Template,
workspace: Workspace,
): TransitionStats | undefined => {
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp