@@ -2,25 +2,28 @@ import { API } from "api/api";
2
2
import { getErrorDetail , getErrorMessage } from "api/errors" ;
3
3
import { template as templateQueryOptions } from "api/queries/templates" ;
4
4
import type { Workspace , WorkspaceStatus } from "api/typesGenerated" ;
5
+ import isChromatic from "chromatic/isChromatic" ;
5
6
import { Button } from "components/Button/Button" ;
6
7
import { Loader } from "components/Loader/Loader" ;
7
8
import { Margins } from "components/Margins/Margins" ;
9
+ import { ScrollArea } from "components/ScrollArea/ScrollArea" ;
8
10
import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs" ;
9
11
import { ArrowLeftIcon , RotateCcwIcon } from "lucide-react" ;
10
12
import { AI_PROMPT_PARAMETER_NAME , type Task } from "modules/tasks/tasks" ;
11
- import type { ReactNode } from "react" ;
13
+ import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs" ;
14
+ import { type FC , type ReactNode , useEffect , useRef } from "react" ;
12
15
import { Helmet } from "react-helmet-async" ;
13
16
import { useQuery } from "react-query" ;
14
17
import { Panel , PanelGroup , PanelResizeHandle } from "react-resizable-panels" ;
15
18
import { Link as RouterLink , useParams } from "react-router" ;
16
- import { ellipsizeText } from "utils/ellipsizeText" ;
17
19
import { pageTitle } from "utils/page" ;
18
20
import {
19
- ActiveTransition ,
21
+ getActiveTransitionStats ,
20
22
WorkspaceBuildProgress ,
21
23
} from "../WorkspacePage/WorkspaceBuildProgress" ;
22
24
import { TaskApps } from "./TaskApps" ;
23
25
import { TaskSidebar } from "./TaskSidebar" ;
26
+ import { TaskTopbar } from "./TaskTopbar" ;
24
27
25
28
const TaskPage = ( ) => {
26
29
const { workspace : workspaceName , username } = useParams ( ) as {
@@ -37,18 +40,7 @@ const TaskPage = () => {
37
40
refetchInterval : 5_000 ,
38
41
} ) ;
39
42
40
- const { data : template } = useQuery ( {
41
- ...templateQueryOptions ( task ?. workspace . template_id ?? "" ) ,
42
- enabled : Boolean ( task ) ,
43
- } ) ;
44
-
45
43
const waitingStatuses : WorkspaceStatus [ ] = [ "starting" , "pending" ] ;
46
- const shouldStreamBuildLogs =
47
- task && waitingStatuses . includes ( task . workspace . latest_build . status ) ;
48
- const buildLogs = useWorkspaceBuildLogs (
49
- task ?. workspace . latest_build . id ?? "" ,
50
- shouldStreamBuildLogs ,
51
- ) ;
52
44
53
45
if ( error ) {
54
46
return (
@@ -95,38 +87,9 @@ const TaskPage = () => {
95
87
}
96
88
97
89
let content : ReactNode = null ;
98
- const _terminatedStatuses : WorkspaceStatus [ ] = [
99
- "canceled" ,
100
- "canceling" ,
101
- "deleted" ,
102
- "deleting" ,
103
- "stopped" ,
104
- "stopping" ,
105
- ] ;
106
90
107
91
if ( waitingStatuses . includes ( task . workspace . latest_build . status ) ) {
108
- // If no template yet, use an indeterminate progress bar.
109
- const transition = ( template &&
110
- ActiveTransition ( template , task . workspace ) ) || { P50 : 0 , P95 : null } ;
111
- const lastStage =
112
- buildLogs ?. [ buildLogs . length - 1 ] ?. stage || "Waiting for build status" ;
113
- content = (
114
- < div className = "w-full min-h-80 flex flex-col" >
115
- < div className = "flex flex-col items-center grow justify-center" >
116
- < h3 className = "m-0 font-medium text-content-primary text-base" >
117
- Starting your workspace
118
- </ h3 >
119
- < div className = "text-content-secondary text-sm" > { lastStage } </ div >
120
- </ div >
121
- < div className = "w-full" >
122
- < WorkspaceBuildProgress
123
- workspace = { task . workspace }
124
- transitionStats = { transition }
125
- variant = "task"
126
- />
127
- </ div >
128
- </ div >
129
- ) ;
92
+ content = < TaskBuildingWorkspace task = { task } /> ;
130
93
} else if ( task . workspace . latest_build . status === "failed" ) {
131
94
content = (
132
95
< div className = "w-full min-h-80 flex items-center justify-center" >
@@ -170,29 +133,103 @@ const TaskPage = () => {
170
133
</ Margins >
171
134
) ;
172
135
} else {
173
- content = < TaskApps task = { task } /> ;
174
- }
175
-
176
- return (
177
- < >
178
- < Helmet >
179
- < title > { pageTitle ( ellipsizeText ( task . prompt , 64 ) ?? "Task" ) } </ title >
180
- </ Helmet >
136
+ content = (
181
137
< PanelGroup autoSaveId = "task" direction = "horizontal" >
182
138
< Panel defaultSize = { 25 } minSize = { 20 } >
183
139
< TaskSidebar task = { task } />
184
140
</ Panel >
185
141
< PanelResizeHandle >
186
142
< div className = "w-1 bg-border h-full hover:bg-border-hover transition-all relative" />
187
143
</ PanelResizeHandle >
188
- < Panel className = "[&>*]:h-full" > { content } </ Panel >
144
+ < Panel className = "[&>*]:h-full" >
145
+ < TaskApps task = { task } />
146
+ </ Panel >
189
147
</ PanelGroup >
148
+ ) ;
149
+ }
150
+
151
+ return (
152
+ < >
153
+ < Helmet >
154
+ < title > { pageTitle ( ellipsizeText ( task . prompt , 64 ) ) } </ title >
155
+ </ Helmet >
156
+
157
+ < div className = "flex flex-col h-full" >
158
+ < TaskTopbar task = { task } />
159
+ { content }
160
+ </ div >
190
161
</ >
191
162
) ;
192
163
} ;
193
164
194
165
export default TaskPage ;
195
166
167
+ type TaskBuildingWorkspaceProps = { task : Task } ;
168
+
169
+ const TaskBuildingWorkspace : FC < TaskBuildingWorkspaceProps > = ( { task } ) => {
170
+ const { data : template } = useQuery (
171
+ templateQueryOptions ( task . workspace . template_id ) ,
172
+ ) ;
173
+
174
+ const buildLogs = useWorkspaceBuildLogs ( task ?. workspace . latest_build . id ) ;
175
+
176
+ // If no template yet, use an indeterminate progress bar.
177
+ const transitionStats = ( template &&
178
+ getActiveTransitionStats ( template , task . workspace ) ) || {
179
+ P50 : 0 ,
180
+ P95 : null ,
181
+ } ;
182
+
183
+ const scrollAreaRef = useRef < HTMLDivElement > ( null ) ;
184
+ // biome-ignore lint/correctness/useExhaustiveDependencies: this effect should run when build logs change
185
+ useEffect ( ( ) => {
186
+ if ( isChromatic ( ) ) {
187
+ return ;
188
+ }
189
+ const scrollAreaEl = scrollAreaRef . current ;
190
+ const scrollAreaViewportEl = scrollAreaEl ?. querySelector < HTMLDivElement > (
191
+ "[data-radix-scroll-area-viewport]" ,
192
+ ) ;
193
+ if ( scrollAreaViewportEl ) {
194
+ scrollAreaViewportEl . scrollTop = scrollAreaViewportEl . scrollHeight ;
195
+ }
196
+ } , [ buildLogs ] ) ;
197
+
198
+ return (
199
+ < section className = "w-full h-full flex justify-center items-center p-6 overflow-y-auto" >
200
+ < div className = "flex flex-col gap-6 items-center w-full" >
201
+ < header className = "flex flex-col items-center text-center" >
202
+ < h3 className = "m-0 font-medium text-content-primary text-xl" >
203
+ Starting your workspace
204
+ </ h3 >
205
+ < div className = "text-content-secondary" >
206
+ Your task will be running in a few moments
207
+ </ div >
208
+ </ header >
209
+
210
+ < div className = "w-full max-w-screen-lg flex flex-col gap-4 overflow-hidden" >
211
+ < WorkspaceBuildProgress
212
+ workspace = { task . workspace }
213
+ transitionStats = { transitionStats }
214
+ variant = "task"
215
+ />
216
+
217
+ < ScrollArea
218
+ ref = { scrollAreaRef }
219
+ className = "h-96 border border-solid border-border rounded-lg"
220
+ >
221
+ < WorkspaceBuildLogs
222
+ sticky
223
+ className = "border-0 rounded-none"
224
+ logs = { buildLogs ?? [ ] }
225
+ />
226
+ </ ScrollArea >
227
+ </ div >
228
+ </ div >
229
+ </ section >
230
+ ) ;
231
+ } ;
232
+
196
233
export class WorkspaceDoesNotHaveAITaskError extends Error {
197
234
constructor ( workspace : Workspace ) {
198
235
super (
@@ -228,3 +265,7 @@ export const data = {
228
265
} satisfies Task ;
229
266
} ,
230
267
} ;
268
+
269
+ const ellipsizeText = ( text : string , maxLength = 80 ) : string => {
270
+ return text . length <= maxLength ? text : `${ text . slice ( 0 , maxLength - 3 ) } ...` ;
271
+ } ;
0 commit comments