8
8
"slices"
9
9
"strings"
10
10
11
+ "github.com/go-chi/chi/v5"
11
12
"github.com/google/uuid"
12
13
13
14
"cdr.dev/slog"
@@ -17,6 +18,8 @@ import (
17
18
"github.com/coder/coder/v2/coderd/httpapi"
18
19
"github.com/coder/coder/v2/coderd/httpmw"
19
20
"github.com/coder/coder/v2/coderd/rbac"
21
+ "github.com/coder/coder/v2/coderd/rbac/policy"
22
+ "github.com/coder/coder/v2/coderd/searchquery"
20
23
"github.com/coder/coder/v2/coderd/taskname"
21
24
"github.com/coder/coder/v2/codersdk"
22
25
)
@@ -186,3 +189,267 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
186
189
defer commitAudit ()
187
190
createWorkspace (ctx , aReq , apiKey .UserID , api , owner , createReq , rw , r )
188
191
}
192
+
193
+ // tasksListResponse wraps a list of experimental tasks.
194
+ //
195
+ // Experimental: Response shape is experimental and may change.
196
+ type tasksListResponse struct {
197
+ Tasks []codersdk.Task `json:"tasks"`
198
+ Count int `json:"count"`
199
+ }
200
+
201
+ func mapTaskStatus (ws codersdk.Workspace ) codersdk.TaskStatus {
202
+ if ws .LatestAppStatus != nil {
203
+ switch ws .LatestAppStatus .State {
204
+ case codersdk .WorkspaceAppStatusStateWorking :
205
+ return codersdk .TaskStatusWorking
206
+ case codersdk .WorkspaceAppStatusStateIdle :
207
+ return codersdk .TaskStatusIdle
208
+ case codersdk .WorkspaceAppStatusStateComplete :
209
+ return codersdk .TaskStatusCompleted
210
+ case codersdk .WorkspaceAppStatusStateFailure :
211
+ return codersdk .TaskStatusFailed
212
+ }
213
+ }
214
+
215
+ switch ws .LatestBuild .Status {
216
+ case codersdk .WorkspaceStatusPending , codersdk .WorkspaceStatusStarting , codersdk .WorkspaceStatusRunning :
217
+ return codersdk .TaskStatusWorking
218
+ case codersdk .WorkspaceStatusStopping , codersdk .WorkspaceStatusStopped , codersdk .WorkspaceStatusDeleting , codersdk .WorkspaceStatusDeleted :
219
+ return codersdk .TaskStatusCompleted
220
+ case codersdk .WorkspaceStatusFailed , codersdk .WorkspaceStatusCanceling , codersdk .WorkspaceStatusCanceled :
221
+ return codersdk .TaskStatusFailed
222
+ default :
223
+ return codersdk .TaskStatusWorking
224
+ }
225
+ }
226
+
227
+ // tasksList is an experimental endpoint to list AI tasks by mapping
228
+ // workspaces to a task-shaped response.
229
+ func (api * API ) tasksList (rw http.ResponseWriter , r * http.Request ) {
230
+ ctx := r .Context ()
231
+ apiKey := httpmw .APIKey (r )
232
+
233
+ // Support standard pagination/filters for workspaces.
234
+ page , ok := ParsePagination (rw , r )
235
+ if ! ok {
236
+ return
237
+ }
238
+ queryStr := r .URL .Query ().Get ("q" )
239
+ filter , errs := searchquery .Workspaces (ctx , api .Database , queryStr , page , api .AgentInactiveDisconnectTimeout )
240
+ if len (errs ) > 0 {
241
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
242
+ Message : "Invalid workspace search query." ,
243
+ Validations : errs ,
244
+ })
245
+ return
246
+ }
247
+
248
+ // Ensure that we only include AI task workspaces in the results.
249
+ filter .HasAITask = sql.NullBool {Valid : true , Bool : true }
250
+
251
+ if filter .OwnerUsername == "me" || filter .OwnerUsername == "" {
252
+ filter .OwnerID = apiKey .UserID
253
+ filter .OwnerUsername = ""
254
+ }
255
+
256
+ prepared , err := api .HTTPAuth .AuthorizeSQLFilter (r , policy .ActionRead , rbac .ResourceWorkspace .Type )
257
+ if err != nil {
258
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
259
+ Message : "Internal error preparing sql filter." ,
260
+ Detail : err .Error (),
261
+ })
262
+ return
263
+ }
264
+
265
+ // Order with requester's favorites first, include summary row.
266
+ filter .RequesterID = apiKey .UserID
267
+ filter .WithSummary = true
268
+
269
+ workspaceRows , err := api .Database .GetAuthorizedWorkspaces (ctx , filter , prepared )
270
+ if err != nil {
271
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
272
+ Message : "Internal error fetching workspaces." ,
273
+ Detail : err .Error (),
274
+ })
275
+ return
276
+ }
277
+ if len (workspaceRows ) == 0 {
278
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
279
+ Message : "Internal error fetching workspaces." ,
280
+ Detail : "Workspace summary row is missing." ,
281
+ })
282
+ return
283
+ }
284
+ if len (workspaceRows ) == 1 {
285
+ httpapi .Write (ctx , rw , http .StatusOK , tasksListResponse {
286
+ Tasks : []codersdk.Task {},
287
+ Count : 0 ,
288
+ })
289
+ return
290
+ }
291
+
292
+ // Skip summary row.
293
+ workspaceRows = workspaceRows [:len (workspaceRows )- 1 ]
294
+
295
+ workspaces := database .ConvertWorkspaceRows (workspaceRows )
296
+
297
+ // Gather associated data and convert to API workspaces.
298
+ data , err := api .workspaceData (ctx , workspaces )
299
+ if err != nil {
300
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
301
+ Message : "Internal error fetching workspace resources." ,
302
+ Detail : err .Error (),
303
+ })
304
+ return
305
+ }
306
+ apiWorkspaces , err := convertWorkspaces (apiKey .UserID , workspaces , data )
307
+ if err != nil {
308
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
309
+ Message : "Internal error converting workspaces." ,
310
+ Detail : err .Error (),
311
+ })
312
+ return
313
+ }
314
+
315
+ // Fetch prompts for each workspace build and map by build ID.
316
+ buildIDs := make ([]uuid.UUID , 0 , len (apiWorkspaces ))
317
+ for _ , ws := range apiWorkspaces {
318
+ buildIDs = append (buildIDs , ws .LatestBuild .ID )
319
+ }
320
+ parameters , err := api .Database .GetWorkspaceBuildParametersByBuildIDs (ctx , buildIDs )
321
+ if err != nil {
322
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
323
+ Message : "Internal error fetching task prompts." ,
324
+ Detail : err .Error (),
325
+ })
326
+ return
327
+ }
328
+ promptsByBuildID := make (map [uuid.UUID ]string , len (parameters ))
329
+ for _ , p := range parameters {
330
+ if p .Name == codersdk .AITaskPromptParameterName {
331
+ promptsByBuildID [p .WorkspaceBuildID ] = p .Value
332
+ }
333
+ }
334
+
335
+ tasks := make ([]codersdk.Task , 0 , len (apiWorkspaces ))
336
+ for _ , ws := range apiWorkspaces {
337
+ tasks = append (tasks , codersdk.Task {
338
+ ID : ws .ID ,
339
+ OrganizationID : ws .OrganizationID ,
340
+ OwnerID : ws .OwnerID ,
341
+ Name : ws .Name ,
342
+ TemplateID : ws .TemplateID ,
343
+ WorkspaceID : uuid.NullUUID {Valid : true , UUID : ws .ID },
344
+ Prompt : promptsByBuildID [ws .LatestBuild .ID ],
345
+ Status : mapTaskStatus (ws ),
346
+ CreatedAt : ws .CreatedAt ,
347
+ UpdatedAt : ws .UpdatedAt ,
348
+ })
349
+ }
350
+
351
+ httpapi .Write (ctx , rw , http .StatusOK , tasksListResponse {
352
+ Tasks : tasks ,
353
+ Count : len (tasks ),
354
+ })
355
+ }
356
+
357
+ // taskGet is an experimental endpoint to fetch a single AI task by ID
358
+ // (workspace ID). It returns a synthesized task response including
359
+ // prompt and status.
360
+ func (api * API ) taskGet (rw http.ResponseWriter , r * http.Request ) {
361
+ ctx := r .Context ()
362
+ apiKey := httpmw .APIKey (r )
363
+
364
+ idStr := chi .URLParam (r , "id" )
365
+ taskID , err := uuid .Parse (idStr )
366
+ if err != nil {
367
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
368
+ Message : fmt .Sprintf ("Invalid UUID %q for task ID." , idStr ),
369
+ })
370
+ return
371
+ }
372
+
373
+ workspace , err := api .Database .GetWorkspaceByID (ctx , taskID )
374
+ if httpapi .Is404Error (err ) {
375
+ httpapi .ResourceNotFound (rw )
376
+ return
377
+ }
378
+ if err != nil {
379
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
380
+ Message : "Internal error fetching workspace." ,
381
+ Detail : err .Error (),
382
+ })
383
+ return
384
+ }
385
+
386
+ data , err := api .workspaceData (ctx , []database.Workspace {workspace })
387
+ if err != nil {
388
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
389
+ Message : "Internal error fetching workspace resources." ,
390
+ Detail : err .Error (),
391
+ })
392
+ return
393
+ }
394
+ if len (data .builds ) == 0 || len (data .templates ) == 0 {
395
+ httpapi .ResourceNotFound (rw )
396
+ return
397
+ }
398
+ if data .builds [0 ].HasAITask == nil || ! * data .builds [0 ].HasAITask {
399
+ httpapi .ResourceNotFound (rw )
400
+ return
401
+ }
402
+
403
+ appStatus := codersdk.WorkspaceAppStatus {}
404
+ if len (data .appStatuses ) > 0 {
405
+ appStatus = data .appStatuses [0 ]
406
+ }
407
+
408
+ ws , err := convertWorkspace (
409
+ apiKey .UserID ,
410
+ workspace ,
411
+ data .builds [0 ],
412
+ data .templates [0 ],
413
+ api .Options .AllowWorkspaceRenames ,
414
+ appStatus ,
415
+ )
416
+ if err != nil {
417
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
418
+ Message : "Internal error converting workspace." ,
419
+ Detail : err .Error (),
420
+ })
421
+ return
422
+ }
423
+
424
+ // Fetch the AI prompt from the build parameters.
425
+ params , err := api .Database .GetWorkspaceBuildParametersByBuildIDs (ctx , []uuid.UUID {ws .LatestBuild .ID })
426
+ if err != nil {
427
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
428
+ Message : "Internal error fetching task prompt." ,
429
+ Detail : err .Error (),
430
+ })
431
+ return
432
+ }
433
+ prompt := ""
434
+ for _ , p := range params {
435
+ if p .Name == codersdk .AITaskPromptParameterName {
436
+ prompt = p .Value
437
+ break
438
+ }
439
+ }
440
+
441
+ resp := codersdk.Task {
442
+ ID : ws .ID ,
443
+ OrganizationID : ws .OrganizationID ,
444
+ OwnerID : ws .OwnerID ,
445
+ Name : ws .Name ,
446
+ TemplateID : ws .TemplateID ,
447
+ WorkspaceID : uuid.NullUUID {Valid : true , UUID : ws .ID },
448
+ Prompt : prompt ,
449
+ Status : mapTaskStatus (ws ),
450
+ CreatedAt : ws .CreatedAt ,
451
+ UpdatedAt : ws .UpdatedAt ,
452
+ }
453
+
454
+ httpapi .Write (ctx , rw , http .StatusOK , resp )
455
+ }
0 commit comments