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
+ }