From a2d782ffa802c210a69f6cd0a474c33a0a909765 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 9 Sep 2025 08:27:45 +0000 Subject: [PATCH 1/2] feat(coderd): allow specifying a name for a task --- coderd/aitasks.go | 28 +++++++++---- coderd/aitasks_test.go | 74 ++++++++++++++++++++++++++++++++++ codersdk/aitasks.go | 1 + site/src/api/typesGenerated.ts | 1 + 4 files changed, 96 insertions(+), 8 deletions(-) diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 1f90a6afda3c9..9f437877e072c 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -113,15 +113,27 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { return } - taskName := taskname.GenerateFallback() - if anthropicAPIKey := taskname.GetAnthropicAPIKeyFromEnv(); anthropicAPIKey != "" { - anthropicModel := taskname.GetAnthropicModelFromEnv() + taskName := req.Name + if err := codersdk.NameValid(taskName); taskName != "" && err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Unable to create a Task with the provided name.", + Detail: err.Error(), + }) + return + } - generatedName, err := taskname.Generate(ctx, req.Prompt, taskname.WithAPIKey(anthropicAPIKey), taskname.WithModel(anthropicModel)) - if err != nil { - api.Logger.Error(ctx, "unable to generate task name", slog.Error(err)) - } else { - taskName = generatedName + if taskName == "" { + taskName = taskname.GenerateFallback() + + if anthropicAPIKey := taskname.GetAnthropicAPIKeyFromEnv(); anthropicAPIKey != "" { + anthropicModel := taskname.GetAnthropicModelFromEnv() + + generatedName, err := taskname.Generate(ctx, req.Prompt, taskname.WithAPIKey(anthropicAPIKey), taskname.WithModel(anthropicModel)) + if err != nil { + api.Logger.Error(ctx, "unable to generate task name", slog.Error(err)) + } else { + taskName = generatedName + } } } diff --git a/coderd/aitasks_test.go b/coderd/aitasks_test.go index 1f3cd8cbbdd08..4b2ae087b687d 100644 --- a/coderd/aitasks_test.go +++ b/coderd/aitasks_test.go @@ -441,6 +441,80 @@ func TestTasksCreate(t *testing.T) { assert.Equal(t, taskPrompt, parameters[0].Value) }) + t.Run("CustomNames", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + taskName string + expectFallbackName bool + expectError string + }{ + { + name: "ValidName", + taskName: "a-valid-task-name", + }, + { + name: "NotValidName", + taskName: "this is not a valid task name", + expectError: "Unable to create a Task with the provided name.", + }, + { + name: "NoNameProvided", + taskName: "", + expectFallbackName: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var ( + ctx = testutil.Context(t, testutil.WaitShort) + client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + expClient = codersdk.NewExperimentalClient(client) + user = coderdtest.CreateFirstUser(t, client) + version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: echo.ApplyComplete, + ProvisionPlan: []*proto.Response{ + {Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ + Parameters: []*proto.RichParameter{{Name: "AI Prompt", Type: "string"}}, + HasAiTasks: true, + }}}, + }, + }) + template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + ) + + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + + // When: We attempt to create a Task. + task, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ + TemplateVersionID: template.ActiveVersionID, + Prompt: "Some prompt", + Name: tt.taskName, + }) + if tt.expectError == "" { + require.NoError(t, err) + require.True(t, task.WorkspaceID.Valid) + + // Then: We expect the correct name to have been picked. + err = codersdk.NameValid(task.Name) + require.NoError(t, err, "Generated task name should be valid") + + require.NotEmpty(t, task.Name) + if !tt.expectFallbackName { + require.Equal(t, tt.taskName, task.Name) + } + } else { + require.ErrorContains(t, err, tt.expectError) + } + }) + } + }) + t.Run("FailsOnNonTaskTemplate", func(t *testing.T) { t.Parallel() diff --git a/codersdk/aitasks.go b/codersdk/aitasks.go index 1ca1016f28ea8..e50448946cd53 100644 --- a/codersdk/aitasks.go +++ b/codersdk/aitasks.go @@ -51,6 +51,7 @@ type CreateTaskRequest struct { TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"` TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` Prompt string `json:"prompt"` + Name string `json:"name,omitempty"` } func (c *ExperimentalClient) CreateTask(ctx context.Context, user string, request CreateTaskRequest) (Task, error) { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 54984cd11548f..98540df857671 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -481,6 +481,7 @@ export interface CreateTaskRequest { readonly template_version_id: string; readonly template_version_preset_id?: string; readonly prompt: string; + readonly name?: string; } // From codersdk/organizations.go From 3104048e78deaba67e006b2d3274709b03236047 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 9 Sep 2025 09:36:35 +0000 Subject: [PATCH 2/2] chore: feedback --- coderd/aitasks.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 9f437877e072c..9c5285fdbc2c1 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -114,12 +114,14 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { } taskName := req.Name - if err := codersdk.NameValid(taskName); taskName != "" && err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Unable to create a Task with the provided name.", - Detail: err.Error(), - }) - return + if taskName != "" { + if err := codersdk.NameValid(taskName); err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Unable to create a Task with the provided name.", + Detail: err.Error(), + }) + return + } } if taskName == "" {