diff --git a/cli/exp_taskcreate.go b/cli/exp_taskcreate.go index 9125b86329..24f0955ea8 100644 --- a/cli/exp_taskcreate.go +++ b/cli/exp_taskcreate.go @@ -104,7 +104,7 @@ func (r *RootCmd) taskCreate() *serpent.Command { templateVersionPresetID = preset.ID } - workspace, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{ + task, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{ TemplateVersionID: templateVersionID, TemplateVersionPresetID: templateVersionPresetID, Prompt: taskInput, @@ -116,8 +116,8 @@ func (r *RootCmd) taskCreate() *serpent.Command { _, _ = fmt.Fprintf( inv.Stdout, "The task %s has been created at %s!\n", - cliui.Keyword(workspace.Name), - cliui.Timestamp(workspace.CreatedAt), + cliui.Keyword(task.Name), + cliui.Timestamp(task.CreatedAt), ) return nil diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 466cedd409..10c3efc961 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -188,7 +188,6 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { WorkspaceOwner: owner.Username, }, }) - defer commitAudit() w, err := createWorkspace(ctx, aReq, apiKey.UserID, api, owner, createReq, r) if err != nil { @@ -196,7 +195,65 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(ctx, rw, http.StatusCreated, w) + task := taskFromWorkspace(w, req.Prompt) + httpapi.Write(ctx, rw, http.StatusCreated, task) +} + +func taskFromWorkspace(ws codersdk.Workspace, initialPrompt string) codersdk.Task { + // TODO(DanielleMaywood): + // This just picks up the first agent it discovers. + // This approach _might_ break when a task has multiple agents, + // depending on which agent was found first. + // + // We explicitly do not have support for running tasks + // inside of a sub agent at the moment, so we can be sure + // that any sub agents are not the agent we're looking for. + var taskAgentID uuid.NullUUID + var taskAgentLifecycle *codersdk.WorkspaceAgentLifecycle + var taskAgentHealth *codersdk.WorkspaceAgentHealth + for _, resource := range ws.LatestBuild.Resources { + for _, agent := range resource.Agents { + if agent.ParentID.Valid { + continue + } + + taskAgentID = uuid.NullUUID{Valid: true, UUID: agent.ID} + taskAgentLifecycle = &agent.LifecycleState + taskAgentHealth = &agent.Health + break + } + } + + var currentState *codersdk.TaskStateEntry + if ws.LatestAppStatus != nil { + currentState = &codersdk.TaskStateEntry{ + Timestamp: ws.LatestAppStatus.CreatedAt, + State: codersdk.TaskState(ws.LatestAppStatus.State), + Message: ws.LatestAppStatus.Message, + URI: ws.LatestAppStatus.URI, + } + } + + return codersdk.Task{ + ID: ws.ID, + OrganizationID: ws.OrganizationID, + OwnerID: ws.OwnerID, + OwnerName: ws.OwnerName, + Name: ws.Name, + TemplateID: ws.TemplateID, + TemplateName: ws.TemplateName, + TemplateDisplayName: ws.TemplateDisplayName, + TemplateIcon: ws.TemplateIcon, + WorkspaceID: uuid.NullUUID{Valid: true, UUID: ws.ID}, + WorkspaceAgentID: taskAgentID, + WorkspaceAgentLifecycle: taskAgentLifecycle, + WorkspaceAgentHealth: taskAgentHealth, + CreatedAt: ws.CreatedAt, + UpdatedAt: ws.UpdatedAt, + InitialPrompt: initialPrompt, + Status: ws.LatestBuild.Status, + CurrentState: currentState, + } } // tasksFromWorkspaces converts a slice of API workspaces into tasks, fetching @@ -221,60 +278,7 @@ func (api *API) tasksFromWorkspaces(ctx context.Context, apiWorkspaces []codersd tasks := make([]codersdk.Task, 0, len(apiWorkspaces)) for _, ws := range apiWorkspaces { - // TODO(DanielleMaywood): - // This just picks up the first agent it discovers. - // This approach _might_ break when a task has multiple agents, - // depending on which agent was found first. - // - // We explicitly do not have support for running tasks - // inside of a sub agent at the moment, so we can be sure - // that any sub agents are not the agent we're looking for. - var taskAgentID uuid.NullUUID - var taskAgentLifecycle *codersdk.WorkspaceAgentLifecycle - var taskAgentHealth *codersdk.WorkspaceAgentHealth - for _, resource := range ws.LatestBuild.Resources { - for _, agent := range resource.Agents { - if agent.ParentID.Valid { - continue - } - - taskAgentID = uuid.NullUUID{Valid: true, UUID: agent.ID} - taskAgentLifecycle = &agent.LifecycleState - taskAgentHealth = &agent.Health - break - } - } - - var currentState *codersdk.TaskStateEntry - if ws.LatestAppStatus != nil { - currentState = &codersdk.TaskStateEntry{ - Timestamp: ws.LatestAppStatus.CreatedAt, - State: codersdk.TaskState(ws.LatestAppStatus.State), - Message: ws.LatestAppStatus.Message, - URI: ws.LatestAppStatus.URI, - } - } - - tasks = append(tasks, codersdk.Task{ - ID: ws.ID, - OrganizationID: ws.OrganizationID, - OwnerID: ws.OwnerID, - OwnerName: ws.OwnerName, - Name: ws.Name, - TemplateID: ws.TemplateID, - TemplateName: ws.TemplateName, - TemplateDisplayName: ws.TemplateDisplayName, - TemplateIcon: ws.TemplateIcon, - WorkspaceID: uuid.NullUUID{Valid: true, UUID: ws.ID}, - WorkspaceAgentID: taskAgentID, - WorkspaceAgentLifecycle: taskAgentLifecycle, - WorkspaceAgentHealth: taskAgentHealth, - CreatedAt: ws.CreatedAt, - UpdatedAt: ws.UpdatedAt, - InitialPrompt: promptsByBuildID[ws.LatestBuild.ID], - Status: ws.LatestBuild.Status, - CurrentState: currentState, - }) + tasks = append(tasks, taskFromWorkspace(ws, promptsByBuildID[ws.LatestBuild.ID])) } return tasks, nil diff --git a/coderd/aitasks_test.go b/coderd/aitasks_test.go index 802d738162..767f52eeab 100644 --- a/coderd/aitasks_test.go +++ b/coderd/aitasks_test.go @@ -419,19 +419,23 @@ func TestTasksCreate(t *testing.T) { expClient := codersdk.NewExperimentalClient(client) // When: We attempt to create a Task. - workspace, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ + task, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ TemplateVersionID: template.ActiveVersionID, Prompt: taskPrompt, }) require.NoError(t, err) - coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + require.True(t, task.WorkspaceID.Valid) + + ws, err := client.Workspace(ctx, task.WorkspaceID.UUID) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) // Then: We expect a workspace to have been created. - assert.NotEmpty(t, workspace.Name) - assert.Equal(t, template.ID, workspace.TemplateID) + assert.NotEmpty(t, task.Name) + assert.Equal(t, template.ID, task.TemplateID) // And: We expect it to have the "AI Prompt" parameter correctly set. - parameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID) + parameters, err := client.WorkspaceBuildParameters(ctx, ws.LatestBuild.ID) require.NoError(t, err) require.Len(t, parameters, 1) assert.Equal(t, codersdk.AITaskPromptParameterName, parameters[0].Name) diff --git a/codersdk/aitasks.go b/codersdk/aitasks.go index 764fd26ae7..1ca1016f28 100644 --- a/codersdk/aitasks.go +++ b/codersdk/aitasks.go @@ -53,23 +53,23 @@ type CreateTaskRequest struct { Prompt string `json:"prompt"` } -func (c *ExperimentalClient) CreateTask(ctx context.Context, user string, request CreateTaskRequest) (Workspace, error) { +func (c *ExperimentalClient) CreateTask(ctx context.Context, user string, request CreateTaskRequest) (Task, error) { res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/tasks/%s", user), request) if err != nil { - return Workspace{}, err + return Task{}, err } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return Workspace{}, ReadBodyAsError(res) + return Task{}, ReadBodyAsError(res) } - var workspace Workspace - if err := json.NewDecoder(res.Body).Decode(&workspace); err != nil { - return Workspace{}, err + var task Task + if err := json.NewDecoder(res.Body).Decode(&task); err != nil { + return Task{}, err } - return workspace, nil + return task, nil } // TaskState represents the high-level lifecycle of a task. diff --git a/site/src/api/api.ts b/site/src/api/api.ts index f1ccef1faf..caf0f5c094 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2686,8 +2686,8 @@ class ExperimentalApiMethods { createTask = async ( user: string, req: TypesGen.CreateTaskRequest, - ): Promise => { - const response = await this.axios.post( + ): Promise => { + const response = await this.axios.post( `/api/experimental/tasks/${user}`, req, ); diff --git a/site/src/pages/TasksPage/TaskPrompt.tsx b/site/src/pages/TasksPage/TaskPrompt.tsx index 13e75dae51..eeffd60ffb 100644 --- a/site/src/pages/TasksPage/TaskPrompt.tsx +++ b/site/src/pages/TasksPage/TaskPrompt.tsx @@ -1,7 +1,9 @@ +import { API } from "api/api"; import { getErrorDetail, getErrorMessage } from "api/errors"; import { templateVersionPresets } from "api/queries/templates"; import type { Preset, + Task, Template, TemplateVersionExternalAuth, } from "api/typesGenerated"; @@ -28,13 +30,12 @@ import { import { useAuthenticated } from "hooks/useAuthenticated"; import { useExternalAuth } from "hooks/useExternalAuth"; import { RedoIcon, RotateCcwIcon, SendIcon } from "lucide-react"; -import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks"; +import { AI_PROMPT_PARAMETER_NAME } from "modules/tasks/tasks"; import { type FC, useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate } from "react-router"; import TextareaAutosize from "react-textarea-autosize"; import { docs } from "utils/docs"; -import { data } from "./data"; const textareaPlaceholder = "Prompt your AI agent to start a task..."; @@ -64,7 +65,7 @@ export const TaskPrompt: FC = ({ { - navigate(`/tasks/${task.workspace.owner_name}/${task.workspace.name}`); + navigate(`/tasks/${task.owner_name}/${task.name}`); }} /> ); @@ -188,12 +189,11 @@ const CreateTaskForm: FC = ({ templates, onSuccess }) => { const createTaskMutation = useMutation({ mutationFn: async ({ prompt }: CreateTaskMutationFnProps) => - data.createTask( + API.experimental.createTask(user.id, { prompt, - user.id, - selectedTemplate.active_version_id, - selectedPresetId, - ), + template_version_id: selectedTemplate.active_version_id, + template_version_preset_id: selectedPresetId, + }), onSuccess: async (task) => { await queryClient.invalidateQueries({ queryKey: ["tasks"], diff --git a/site/src/pages/TasksPage/TasksPage.stories.tsx b/site/src/pages/TasksPage/TasksPage.stories.tsx index a10e4f29e7..059d76eb20 100644 --- a/site/src/pages/TasksPage/TasksPage.stories.tsx +++ b/site/src/pages/TasksPage/TasksPage.stories.tsx @@ -2,6 +2,7 @@ import { MockAIPromptPresets, MockNewTaskData, MockPresets, + MockTask, MockTasks, MockTemplate, MockTemplateVersionExternalAuthGithub, @@ -19,7 +20,6 @@ import { API } from "api/api"; import { MockUsers } from "pages/UsersPage/storybookData/users"; import { expect, spyOn, userEvent, waitFor, within } from "storybook/test"; import { reactRouterParameters } from "storybook-addon-remix-react-router"; -import { data } from "./data"; import TasksPage from "./TasksPage"; const meta: Meta = { @@ -248,7 +248,7 @@ export const CreateTaskSuccessfully: Story = { spyOn(API.experimental, "getTasks") .mockResolvedValueOnce(MockTasks) .mockResolvedValue([MockNewTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); + spyOn(API.experimental, "createTask").mockResolvedValue(MockTask); }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); @@ -272,7 +272,7 @@ export const CreateTaskError: Story = { beforeEach: () => { spyOn(API, "getTemplates").mockResolvedValue([MockTemplate]); spyOn(API.experimental, "getTasks").mockResolvedValue(MockTasks); - spyOn(data, "createTask").mockRejectedValue( + spyOn(API.experimental, "createTask").mockRejectedValue( mockApiError({ message: "Failed to create task", detail: "You don't have permission to create tasks.", @@ -301,7 +301,7 @@ export const WithAuthenticatedExternalAuth: Story = { spyOn(API.experimental, "getTasks") .mockResolvedValueOnce(MockTasks) .mockResolvedValue([MockNewTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); + spyOn(API.experimental, "createTask").mockResolvedValue(MockTask); spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([ MockTemplateVersionExternalAuthGithubAuthenticated, ]); @@ -327,7 +327,7 @@ export const MissingExternalAuth: Story = { spyOn(API.experimental, "getTasks") .mockResolvedValueOnce(MockTasks) .mockResolvedValue([MockNewTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); + spyOn(API.experimental, "createTask").mockResolvedValue(MockTask); spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([ MockTemplateVersionExternalAuthGithub, ]); @@ -353,7 +353,7 @@ export const ExternalAuthError: Story = { spyOn(API.experimental, "getTasks") .mockResolvedValueOnce(MockTasks) .mockResolvedValue([MockNewTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); + spyOn(API.experimental, "createTask").mockResolvedValue(MockTask); spyOn(API, "getTemplateVersionExternalAuth").mockRejectedValue( mockApiError({ message: "Failed to load external auth", diff --git a/site/src/pages/TasksPage/data.ts b/site/src/pages/TasksPage/data.ts deleted file mode 100644 index 0795dab2bb..0000000000 --- a/site/src/pages/TasksPage/data.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { API } from "api/api"; -import type { Task } from "modules/tasks/tasks"; - -// TODO: This is a temporary solution while the BE does not return the Task in a -// right shape with a custom name. This should be removed once the BE is fixed. -export const data = { - async createTask( - prompt: string, - userId: string, - templateVersionId: string, - presetId: string | undefined, - ): Promise { - const workspace = await API.experimental.createTask(userId, { - template_version_id: templateVersionId, - template_version_preset_id: presetId, - prompt, - }); - - return { - workspace, - prompt, - }; - }, -}; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 993b012bc0..fb7ab29659 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -4903,6 +4903,32 @@ export const MockTasks = [ }, ]; +export const MockTask: TypesGen.Task = { + id: "test-task", + name: "task-wild-test-123", + organization_id: MockOrganization.id, + owner_id: MockUserOwner.id, + owner_name: MockUserOwner.username, + template_id: MockTemplate.id, + template_name: MockTemplate.name, + template_display_name: MockTemplate.display_name, + template_icon: MockTemplate.icon, + workspace_id: MockWorkspace.id, + workspace_agent_id: MockWorkspaceAgent.id, + workspace_agent_lifecycle: MockWorkspaceAgent.lifecycle_state, + workspace_agent_health: MockWorkspaceAgent.health, + initial_prompt: "Perform some task", + status: "running", + current_state: { + timestamp: "2022-05-17T17:39:01.382927298Z", + state: "idle", + message: "Should I continue?", + uri: "https://dev.coder.com", + }, + created_at: "2022-05-17T17:39:01.382927298Z", + updated_at: "2022-05-17T17:39:01.382927298Z", +}; + export const MockNewTaskData = { prompt: "Create a new task", workspace: {