fix: deprecate codersdk.AITaskPromptParameterName and reduce usage (#20501)

Depends on coder/sqlc#1
Fixes coder/internal#979
Updates coder/internal#973
This commit is contained in:
Mathias Fredriksson
2025-10-29 20:59:12 +02:00
committed by GitHub
parent 50749d131b
commit 859e94d67a
13 changed files with 163 additions and 447 deletions
+9 -67
View File
@@ -2,7 +2,6 @@ package cli_test
import ( import (
"bytes" "bytes"
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"io" "io"
@@ -19,10 +18,7 @@ import (
"github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest" "github.com/coder/coder/v2/pty/ptytest"
@@ -43,76 +39,22 @@ func makeAITask(t *testing.T, db database.Store, orgID, adminID, ownerID uuid.UU
}, },
}).Do() }).Do()
ws := database.WorkspaceTable{ build := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: orgID, OrganizationID: orgID,
OwnerID: ownerID, OwnerID: ownerID,
TemplateID: tv.Template.ID, TemplateID: tv.Template.ID,
} }).
build := dbfake.WorkspaceBuild(t, db, ws).
Seed(database.WorkspaceBuild{ Seed(database.WorkspaceBuild{
TemplateVersionID: tv.TemplateVersion.ID, TemplateVersionID: tv.TemplateVersion.ID,
Transition: transition, Transition: transition,
}).WithAgent().Do() }).
dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{ WithAgent().
{ WithTask(database.TaskTable{
WorkspaceBuildID: build.Build.ID, Prompt: prompt,
Name: codersdk.AITaskPromptParameterName, }, nil).
Value: prompt, Do()
},
})
agents, err := db.GetWorkspaceAgentsByWorkspaceAndBuildNumber(
dbauthz.AsSystemRestricted(context.Background()),
database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams{
WorkspaceID: build.Workspace.ID,
BuildNumber: build.Build.BuildNumber,
},
)
require.NoError(t, err)
require.NotEmpty(t, agents)
agentID := agents[0].ID
// Create a workspace app and set it as the sidebar app. return build.Task
app := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{
AgentID: agentID,
Slug: "task-sidebar",
DisplayName: "Task Sidebar",
External: false,
})
// Update build flags to reference the sidebar app and HasAITask=true.
err = db.UpdateWorkspaceBuildFlagsByID(
dbauthz.AsSystemRestricted(context.Background()),
database.UpdateWorkspaceBuildFlagsByIDParams{
ID: build.Build.ID,
HasAITask: sql.NullBool{Bool: true, Valid: true},
HasExternalAgent: sql.NullBool{Bool: false, Valid: false},
SidebarAppID: uuid.NullUUID{UUID: app.ID, Valid: true},
UpdatedAt: build.Build.UpdatedAt,
},
)
require.NoError(t, err)
// Create a task record in the tasks table for the new data model.
task := dbgen.Task(t, db, database.TaskTable{
OrganizationID: orgID,
OwnerID: ownerID,
Name: build.Workspace.Name,
WorkspaceID: uuid.NullUUID{UUID: build.Workspace.ID, Valid: true},
TemplateVersionID: tv.TemplateVersion.ID,
TemplateParameters: []byte("{}"),
Prompt: prompt,
CreatedAt: dbtime.Now(),
})
// Link the task to the workspace app.
dbgen.TaskWorkspaceApp(t, db, database.TaskWorkspaceApp{
TaskID: task.ID,
WorkspaceBuildNumber: build.Build.BuildNumber,
WorkspaceAgentID: uuid.NullUUID{UUID: agentID, Valid: true},
WorkspaceAppID: uuid.NullUUID{UUID: app.ID, Valid: true},
})
return task
} }
func TestExpTaskList(t *testing.T) { func TestExpTaskList(t *testing.T) {
+1 -4
View File
@@ -293,7 +293,6 @@ func createAITaskTemplate(t *testing.T, client *codersdk.Client, orgID uuid.UUID
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{ Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
HasAiTasks: true, HasAiTasks: true,
}, },
}, },
@@ -328,9 +327,7 @@ func createAITaskTemplate(t *testing.T, client *codersdk.Client, orgID uuid.UUID
}, },
AiTasks: []*proto.AITask{ AiTasks: []*proto.AITask{
{ {
SidebarApp: &proto.AITaskSidebarApp{ AppId: taskAppID.String(),
Id: taskAppID.String(),
},
}, },
}, },
}, },
+22 -55
View File
@@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"slices" "slices"
"strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@@ -24,62 +23,12 @@ import (
"github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/searchquery" "github.com/coder/coder/v2/coderd/searchquery"
"github.com/coder/coder/v2/coderd/taskname" "github.com/coder/coder/v2/coderd/taskname"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
aiagentapi "github.com/coder/agentapi-sdk-go" aiagentapi "github.com/coder/agentapi-sdk-go"
) )
// This endpoint is experimental and not guaranteed to be stable, so we're not
// generating public-facing documentation for it.
func (api *API) aiTasksPrompts(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
buildIDsParam := r.URL.Query().Get("build_ids")
if buildIDsParam == "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "build_ids query parameter is required",
})
return
}
// Parse build IDs
buildIDStrings := strings.Split(buildIDsParam, ",")
buildIDs := make([]uuid.UUID, 0, len(buildIDStrings))
for _, idStr := range buildIDStrings {
id, err := uuid.Parse(strings.TrimSpace(idStr))
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Invalid build ID format: %s", idStr),
Detail: err.Error(),
})
return
}
buildIDs = append(buildIDs, id)
}
parameters, err := api.Database.GetWorkspaceBuildParametersByBuildIDs(ctx, buildIDs)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace build parameters.",
Detail: err.Error(),
})
return
}
promptsByBuildID := make(map[string]string, len(parameters))
for _, param := range parameters {
if param.Name != codersdk.AITaskPromptParameterName {
continue
}
buildID := param.WorkspaceBuildID.String()
promptsByBuildID[buildID] = param.Value
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AITasksPromptsResponse{
Prompts: promptsByBuildID,
})
}
// @Summary Create a new AI task // @Summary Create a new AI task
// @Description: EXPERIMENTAL: this endpoint is experimental and not guaranteed to be stable. // @Description: EXPERIMENTAL: this endpoint is experimental and not guaranteed to be stable.
// @ID create-task // @ID create-task
@@ -174,13 +123,31 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
} }
} }
// Check if the template defines the AI Prompt parameter.
templateParams, err := api.Database.GetTemplateVersionParameters(ctx, req.TemplateVersionID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template parameters.",
Detail: err.Error(),
})
return
}
var richParams []codersdk.WorkspaceBuildParameter
if _, hasAIPromptParam := slice.Find(templateParams, func(param database.TemplateVersionParameter) bool {
return param.Name == codersdk.AITaskPromptParameterName
}); hasAIPromptParam {
// Only add the AI Prompt parameter if the template defines it.
richParams = []codersdk.WorkspaceBuildParameter{
{Name: codersdk.AITaskPromptParameterName, Value: req.Input},
}
}
createReq := codersdk.CreateWorkspaceRequest{ createReq := codersdk.CreateWorkspaceRequest{
Name: taskName, Name: taskName,
TemplateVersionID: req.TemplateVersionID, TemplateVersionID: req.TemplateVersionID,
TemplateVersionPresetID: req.TemplateVersionPresetID, TemplateVersionPresetID: req.TemplateVersionPresetID,
RichParameterValues: []codersdk.WorkspaceBuildParameter{ RichParameterValues: richParams,
{Name: codersdk.AITaskPromptParameterName, Value: req.Input},
},
} }
var owner workspaceOwner var owner workspaceOwner
+45 -129
View File
@@ -35,128 +35,6 @@ import (
"github.com/coder/coder/v2/testutil" "github.com/coder/coder/v2/testutil"
) )
func TestAITasksPrompts(t *testing.T) {
t.Parallel()
t.Run("EmptyBuildIDs", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{})
_ = coderdtest.CreateFirstUser(t, client)
experimentalClient := codersdk.NewExperimentalClient(client)
ctx := testutil.Context(t, testutil.WaitShort)
// Test with empty build IDs
prompts, err := experimentalClient.AITaskPrompts(ctx, []uuid.UUID{})
require.NoError(t, err)
require.Empty(t, prompts.Prompts)
})
t.Run("MultipleBuilds", func(t *testing.T) {
t.Parallel()
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
first := coderdtest.CreateFirstUser(t, adminClient)
memberClient, _ := coderdtest.CreateAnotherUser(t, adminClient, first.OrganizationID)
ctx := testutil.Context(t, testutil.WaitLong)
// Create a template with parameters
version := coderdtest.CreateTemplateVersion(t, adminClient, first.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{
{
Name: "param1",
Type: "string",
DefaultValue: "default1",
},
{
Name: codersdk.AITaskPromptParameterName,
Type: "string",
DefaultValue: "default2",
},
},
},
},
}},
ProvisionApply: echo.ApplyComplete,
})
template := coderdtest.CreateTemplate(t, adminClient, first.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, version.ID)
// Create two workspaces with different parameters
workspace1 := coderdtest.CreateWorkspace(t, memberClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{Name: "param1", Value: "value1a"},
{Name: codersdk.AITaskPromptParameterName, Value: "value2a"},
}
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace1.LatestBuild.ID)
workspace2 := coderdtest.CreateWorkspace(t, memberClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{Name: "param1", Value: "value1b"},
{Name: codersdk.AITaskPromptParameterName, Value: "value2b"},
}
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace2.LatestBuild.ID)
workspace3 := coderdtest.CreateWorkspace(t, adminClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{Name: "param1", Value: "value1c"},
{Name: codersdk.AITaskPromptParameterName, Value: "value2c"},
}
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, adminClient, workspace3.LatestBuild.ID)
allBuildIDs := []uuid.UUID{workspace1.LatestBuild.ID, workspace2.LatestBuild.ID, workspace3.LatestBuild.ID}
experimentalMemberClient := codersdk.NewExperimentalClient(memberClient)
// Test parameters endpoint as member
prompts, err := experimentalMemberClient.AITaskPrompts(ctx, allBuildIDs)
require.NoError(t, err)
// we expect 2 prompts because the member client does not have access to workspace3
// since it was created by the admin client
require.Len(t, prompts.Prompts, 2)
// Check workspace1 parameters
build1Prompt := prompts.Prompts[workspace1.LatestBuild.ID.String()]
require.Equal(t, "value2a", build1Prompt)
// Check workspace2 parameters
build2Prompt := prompts.Prompts[workspace2.LatestBuild.ID.String()]
require.Equal(t, "value2b", build2Prompt)
experimentalAdminClient := codersdk.NewExperimentalClient(adminClient)
// Test parameters endpoint as admin
// we expect 3 prompts because the admin client has access to all workspaces
prompts, err = experimentalAdminClient.AITaskPrompts(ctx, allBuildIDs)
require.NoError(t, err)
require.Len(t, prompts.Prompts, 3)
// Check workspace3 parameters
build3Prompt := prompts.Prompts[workspace3.LatestBuild.ID.String()]
require.Equal(t, "value2c", build3Prompt)
})
t.Run("NonExistentBuildIDs", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{})
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
// Test with non-existent build IDs
nonExistentID := uuid.New()
experimentalClient := codersdk.NewExperimentalClient(client)
prompts, err := experimentalClient.AITaskPrompts(ctx, []uuid.UUID{nonExistentID})
require.NoError(t, err)
require.Empty(t, prompts.Prompts)
})
}
func TestTasks(t *testing.T) { func TestTasks(t *testing.T) {
t.Parallel() t.Parallel()
@@ -188,7 +66,6 @@ func TestTasks(t *testing.T) {
{ {
Type: &proto.Response_Plan{ Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{ Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
HasAiTasks: true, HasAiTasks: true,
}, },
}, },
@@ -817,6 +694,51 @@ func TestTasksCreate(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, 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{
HasAiTasks: true,
}}},
},
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
expClient := codersdk.NewExperimentalClient(client)
task, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{
TemplateVersionID: template.ActiveVersionID,
Input: taskPrompt,
})
require.NoError(t, err)
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)
assert.NotEmpty(t, task.Name)
assert.Equal(t, template.ID, task.TemplateID)
parameters, err := client.WorkspaceBuildParameters(ctx, ws.LatestBuild.ID)
require.NoError(t, err)
require.Len(t, parameters, 0)
})
t.Run("OK AIPromptBackCompat", func(t *testing.T) {
t.Parallel()
var (
ctx = testutil.Context(t, testutil.WaitShort)
taskPrompt = "Some task prompt"
)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
// Given: A template with an "AI Prompt" parameter // Given: A template with an "AI Prompt" parameter
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
@@ -896,7 +818,6 @@ func TestTasksCreate(t *testing.T) {
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -1012,7 +933,6 @@ func TestTasksCreate(t *testing.T) {
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -1072,7 +992,6 @@ func TestTasksCreate(t *testing.T) {
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -1109,7 +1028,6 @@ func TestTasksCreate(t *testing.T) {
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -1162,7 +1080,6 @@ func TestTasksCreate(t *testing.T) {
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
@@ -1175,7 +1092,6 @@ func TestTasksCreate(t *testing.T) {
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
ProvisionPlan: []*proto.Response{ ProvisionPlan: []*proto.Response{
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{ {Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
HasAiTasks: true, HasAiTasks: true,
}}}, }}},
}, },
+1 -4
View File
@@ -1021,10 +1021,7 @@ func New(options *Options) *API {
apiRateLimiter, apiRateLimiter,
httpmw.ReportCLITelemetry(api.Logger, options.Telemetry), httpmw.ReportCLITelemetry(api.Logger, options.Telemetry),
) )
r.Route("/aitasks", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Get("/prompts", api.aiTasksPrompts)
})
r.Route("/tasks", func(r chi.Router) { r.Route("/tasks", func(r chi.Router) {
r.Use(apiKeyMiddleware) r.Use(apiKeyMiddleware)
-1
View File
@@ -329,7 +329,6 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
&i.LatestBuildError, &i.LatestBuildError,
&i.LatestBuildTransition, &i.LatestBuildTransition,
&i.LatestBuildStatus, &i.LatestBuildStatus,
&i.LatestBuildHasAITask,
&i.LatestBuildHasExternalAgent, &i.LatestBuildHasExternalAgent,
&i.Count, &i.Count,
); err != nil { ); err != nil {
+15 -25
View File
@@ -22289,7 +22289,6 @@ SELECT
latest_build.error as latest_build_error, latest_build.error as latest_build_error,
latest_build.transition as latest_build_transition, latest_build.transition as latest_build_transition,
latest_build.job_status as latest_build_status, latest_build.job_status as latest_build_status,
latest_build.has_ai_task as latest_build_has_ai_task,
latest_build.has_external_agent as latest_build_has_external_agent latest_build.has_external_agent as latest_build_has_external_agent
FROM FROM
workspaces_expanded as workspaces workspaces_expanded as workspaces
@@ -22523,25 +22522,19 @@ WHERE
(latest_build.template_version_id = template.active_version_id) = $18 :: boolean (latest_build.template_version_id = template.active_version_id) = $18 :: boolean
ELSE true ELSE true
END END
-- Filter by has_ai_task in latest build -- Filter by has_ai_task, checks if this is a task workspace.
AND CASE AND CASE
WHEN $19 :: boolean IS NOT NULL THEN WHEN $19::boolean IS NOT NULL
(COALESCE(latest_build.has_ai_task, false) OR ( THEN $19::boolean = EXISTS (
-- If the build has no AI task, it means that the provisioner job is in progress SELECT
-- and we don't know if it has an AI task yet. In this case, we optimistically 1
-- assume that it has an AI task if the AI Prompt parameter is not empty. This FROM
-- lets the AI Task frontend spawn a task and see it immediately after instead of tasks
-- having to wait for the build to complete. WHERE
latest_build.has_ai_task IS NULL AND -- Consider all tasks, deleting a task does not turn the
latest_build.completed_at IS NULL AND -- workspace into a non-task workspace.
EXISTS ( tasks.workspace_id = workspaces.id
SELECT 1 )
FROM workspace_build_parameters
WHERE workspace_build_parameters.workspace_build_id = latest_build.id
AND workspace_build_parameters.name = 'AI Prompt'
AND workspace_build_parameters.value != ''
)
)) = ($19 :: boolean)
ELSE true ELSE true
END END
-- Filter by has_external_agent in latest build -- Filter by has_external_agent in latest build
@@ -22572,7 +22565,7 @@ WHERE
-- @authorize_filter -- @authorize_filter
), filtered_workspaces_order AS ( ), filtered_workspaces_order AS (
SELECT SELECT
fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.task_id, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_ai_task, fw.latest_build_has_external_agent fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.task_id, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_external_agent
FROM FROM
filtered_workspaces fw filtered_workspaces fw
ORDER BY ORDER BY
@@ -22593,7 +22586,7 @@ WHERE
$25 $25
), filtered_workspaces_order_with_summary AS ( ), filtered_workspaces_order_with_summary AS (
SELECT SELECT
fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.task_id, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_ai_task, fwo.latest_build_has_external_agent fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.task_id, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_external_agent
FROM FROM
filtered_workspaces_order fwo filtered_workspaces_order fwo
-- Return a technical summary row with total count of workspaces. -- Return a technical summary row with total count of workspaces.
@@ -22638,7 +22631,6 @@ WHERE
'', -- latest_build_error '', -- latest_build_error
'start'::workspace_transition, -- latest_build_transition 'start'::workspace_transition, -- latest_build_transition
'unknown'::provisioner_job_status, -- latest_build_status 'unknown'::provisioner_job_status, -- latest_build_status
false, -- latest_build_has_ai_task
false -- latest_build_has_external_agent false -- latest_build_has_external_agent
WHERE WHERE
$27 :: boolean = true $27 :: boolean = true
@@ -22649,7 +22641,7 @@ WHERE
filtered_workspaces filtered_workspaces
) )
SELECT SELECT
fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.task_id, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_ai_task, fwos.latest_build_has_external_agent, fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.task_id, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_external_agent,
tc.count tc.count
FROM FROM
filtered_workspaces_order_with_summary fwos filtered_workspaces_order_with_summary fwos
@@ -22725,7 +22717,6 @@ type GetWorkspacesRow struct {
LatestBuildError sql.NullString `db:"latest_build_error" json:"latest_build_error"` LatestBuildError sql.NullString `db:"latest_build_error" json:"latest_build_error"`
LatestBuildTransition WorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"` LatestBuildTransition WorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"`
LatestBuildStatus ProvisionerJobStatus `db:"latest_build_status" json:"latest_build_status"` LatestBuildStatus ProvisionerJobStatus `db:"latest_build_status" json:"latest_build_status"`
LatestBuildHasAITask sql.NullBool `db:"latest_build_has_ai_task" json:"latest_build_has_ai_task"`
LatestBuildHasExternalAgent sql.NullBool `db:"latest_build_has_external_agent" json:"latest_build_has_external_agent"` LatestBuildHasExternalAgent sql.NullBool `db:"latest_build_has_external_agent" json:"latest_build_has_external_agent"`
Count int64 `db:"count" json:"count"` Count int64 `db:"count" json:"count"`
} }
@@ -22808,7 +22799,6 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
&i.LatestBuildError, &i.LatestBuildError,
&i.LatestBuildTransition, &i.LatestBuildTransition,
&i.LatestBuildStatus, &i.LatestBuildStatus,
&i.LatestBuildHasAITask,
&i.LatestBuildHasExternalAgent, &i.LatestBuildHasExternalAgent,
&i.Count, &i.Count,
); err != nil { ); err != nil {
+12 -20
View File
@@ -117,7 +117,6 @@ SELECT
latest_build.error as latest_build_error, latest_build.error as latest_build_error,
latest_build.transition as latest_build_transition, latest_build.transition as latest_build_transition,
latest_build.job_status as latest_build_status, latest_build.job_status as latest_build_status,
latest_build.has_ai_task as latest_build_has_ai_task,
latest_build.has_external_agent as latest_build_has_external_agent latest_build.has_external_agent as latest_build_has_external_agent
FROM FROM
workspaces_expanded as workspaces workspaces_expanded as workspaces
@@ -351,25 +350,19 @@ WHERE
(latest_build.template_version_id = template.active_version_id) = sqlc.narg('using_active') :: boolean (latest_build.template_version_id = template.active_version_id) = sqlc.narg('using_active') :: boolean
ELSE true ELSE true
END END
-- Filter by has_ai_task in latest build -- Filter by has_ai_task, checks if this is a task workspace.
AND CASE AND CASE
WHEN sqlc.narg('has_ai_task') :: boolean IS NOT NULL THEN WHEN sqlc.narg('has_ai_task')::boolean IS NOT NULL
(COALESCE(latest_build.has_ai_task, false) OR ( THEN sqlc.narg('has_ai_task')::boolean = EXISTS (
-- If the build has no AI task, it means that the provisioner job is in progress SELECT
-- and we don't know if it has an AI task yet. In this case, we optimistically 1
-- assume that it has an AI task if the AI Prompt parameter is not empty. This FROM
-- lets the AI Task frontend spawn a task and see it immediately after instead of tasks
-- having to wait for the build to complete. WHERE
latest_build.has_ai_task IS NULL AND -- Consider all tasks, deleting a task does not turn the
latest_build.completed_at IS NULL AND -- workspace into a non-task workspace.
EXISTS ( tasks.workspace_id = workspaces.id
SELECT 1 )
FROM workspace_build_parameters
WHERE workspace_build_parameters.workspace_build_id = latest_build.id
AND workspace_build_parameters.name = 'AI Prompt'
AND workspace_build_parameters.value != ''
)
)) = (sqlc.narg('has_ai_task') :: boolean)
ELSE true ELSE true
END END
-- Filter by has_external_agent in latest build -- Filter by has_external_agent in latest build
@@ -466,7 +459,6 @@ WHERE
'', -- latest_build_error '', -- latest_build_error
'start'::workspace_transition, -- latest_build_transition 'start'::workspace_transition, -- latest_build_transition
'unknown'::provisioner_job_status, -- latest_build_status 'unknown'::provisioner_job_status, -- latest_build_status
false, -- latest_build_has_ai_task
false -- latest_build_has_external_agent false -- latest_build_has_external_agent
WHERE WHERE
@with_summary :: boolean = true @with_summary :: boolean = true
+40 -66
View File
@@ -4700,11 +4700,16 @@ func TestWorkspaceFilterHasAITask(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong) ctx := testutil.Context(t, testutil.WaitLong)
// Helper function to create workspace with AI task configuration // Helper function to create workspace with optional task.
createWorkspaceWithAIConfig := func(hasAITask sql.NullBool, jobCompleted bool, aiTaskPrompt *string) database.WorkspaceTable { createWorkspace := func(jobCompleted, createTask bool, prompt string) uuid.UUID {
// TODO(mafredri): The bellow comment is based on deprecated logic and
// kept only present to test that the old observable behavior works as
// intended.
//
// When a provisioner job uses these tags, no provisioner will match it. // When a provisioner job uses these tags, no provisioner will match it.
// We do this so jobs will always be stuck in "pending", allowing us to exercise the intermediary state when // We do this so jobs will always be stuck in "pending", allowing us to
// has_ai_task is nil and we compensate by looking at pending provisioning jobs. // exercise the intermediary state when has_ai_task is nil and we
// compensate by looking at pending provisioning jobs.
// See GetWorkspaces clauses. // See GetWorkspaces clauses.
unpickableTags := database.StringMap{"custom": "true"} unpickableTags := database.StringMap{"custom": "true"}
@@ -4723,102 +4728,71 @@ func TestWorkspaceFilterHasAITask(t *testing.T) {
jobConfig.CompletedAt = sql.NullTime{Time: time.Now(), Valid: true} jobConfig.CompletedAt = sql.NullTime{Time: time.Now(), Valid: true}
} }
job := dbgen.ProvisionerJob(t, db, pubsub, jobConfig) job := dbgen.ProvisionerJob(t, db, pubsub, jobConfig)
res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: job.ID}) res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: job.ID})
agnt := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res.ID}) agnt := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res.ID})
taskApp := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agnt.ID})
var sidebarAppID uuid.UUID
if hasAITask.Bool {
sidebarApp := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agnt.ID})
sidebarAppID = sidebarApp.ID
}
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: ws.ID, WorkspaceID: ws.ID,
TemplateVersionID: version.ID, TemplateVersionID: version.ID,
InitiatorID: user.UserID, InitiatorID: user.UserID,
JobID: job.ID, JobID: job.ID,
BuildNumber: 1, BuildNumber: 1,
HasAITask: hasAITask, AITaskSidebarAppID: uuid.NullUUID{UUID: taskApp.ID, Valid: createTask},
AITaskSidebarAppID: uuid.NullUUID{UUID: sidebarAppID, Valid: sidebarAppID != uuid.Nil},
}) })
if aiTaskPrompt != nil { if createTask {
err := db.InsertWorkspaceBuildParameters(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceBuildParametersParams{ task := dbgen.Task(t, db, database.TaskTable{
WorkspaceBuildID: build.ID, WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true},
Name: []string{provider.TaskPromptParameterName}, OrganizationID: user.OrganizationID,
Value: []string{*aiTaskPrompt}, OwnerID: user.UserID,
TemplateVersionID: version.ID,
Prompt: prompt,
})
dbgen.TaskWorkspaceApp(t, db, database.TaskWorkspaceApp{
TaskID: task.ID,
WorkspaceBuildNumber: build.BuildNumber,
WorkspaceAgentID: uuid.NullUUID{UUID: agnt.ID, Valid: true},
WorkspaceAppID: uuid.NullUUID{UUID: taskApp.ID, Valid: true},
}) })
require.NoError(t, err)
} }
return ws return ws.ID
} }
// Create test workspaces with different AI task configurations // Create workspaces with tasks.
wsWithAITask := createWorkspaceWithAIConfig(sql.NullBool{Bool: true, Valid: true}, true, nil) wsWithTask1 := createWorkspace(true, true, "Build me a web app")
wsWithoutAITask := createWorkspaceWithAIConfig(sql.NullBool{Bool: false, Valid: true}, false, nil) wsWithTask2 := createWorkspace(false, true, "Another task")
aiTaskPrompt := "Build me a web app" // Create workspaces without tasks
wsWithAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, false, &aiTaskPrompt) wsWithoutTask1 := createWorkspace(true, false, "")
wsWithoutTask2 := createWorkspace(false, false, "")
anotherTaskPrompt := "Another task"
wsCompletedWithAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, true, &anotherTaskPrompt)
emptyPrompt := ""
wsWithEmptyAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, false, &emptyPrompt)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// Debug: Check all workspaces without filter first
allRes, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
require.NoError(t, err)
t.Logf("Total workspaces created: %d", len(allRes.Workspaces))
for i, ws := range allRes.Workspaces {
t.Logf("All Workspace %d: ID=%s, Name=%s, Build ID=%s, Job ID=%s", i, ws.ID, ws.Name, ws.LatestBuild.ID, ws.LatestBuild.Job.ID)
}
// Test filtering for workspaces with AI tasks // Test filtering for workspaces with AI tasks
// Should include: wsWithAITask (has_ai_task=true) and wsWithAITaskParam (null + incomplete + param) // Should include: wsWithTask1 and wsWithTask2
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
FilterQuery: "has-ai-task:true", FilterQuery: "has-ai-task:true",
}) })
require.NoError(t, err) require.NoError(t, err)
t.Logf("Expected 2 workspaces for has-ai-task:true, got %d", len(res.Workspaces))
t.Logf("Expected workspaces: %s, %s", wsWithAITask.ID, wsWithAITaskParam.ID)
for i, ws := range res.Workspaces {
t.Logf("AI Task True Workspace %d: ID=%s, Name=%s", i, ws.ID, ws.Name)
}
require.Len(t, res.Workspaces, 2) require.Len(t, res.Workspaces, 2)
workspaceIDs := []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID} workspaceIDs := []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID}
require.Contains(t, workspaceIDs, wsWithAITask.ID) require.Contains(t, workspaceIDs, wsWithTask1)
require.Contains(t, workspaceIDs, wsWithAITaskParam.ID) require.Contains(t, workspaceIDs, wsWithTask2)
// Test filtering for workspaces without AI tasks // Test filtering for workspaces without AI tasks
// Should include: wsWithoutAITask, wsCompletedWithAITaskParam, wsWithEmptyAITaskParam // Should include: wsWithoutTask1, wsWithoutTask2, wsWithoutTask3
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{ res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
FilterQuery: "has-ai-task:false", FilterQuery: "has-ai-task:false",
}) })
require.NoError(t, err) require.NoError(t, err)
require.Len(t, res.Workspaces, 2)
// Debug: print what we got workspaceIDs = []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID}
t.Logf("Expected 3 workspaces for has-ai-task:false, got %d", len(res.Workspaces)) require.Contains(t, workspaceIDs, wsWithoutTask1)
for i, ws := range res.Workspaces { require.Contains(t, workspaceIDs, wsWithoutTask2)
t.Logf("Workspace %d: ID=%s, Name=%s", i, ws.ID, ws.Name)
}
t.Logf("Expected IDs: %s, %s, %s", wsWithoutAITask.ID, wsCompletedWithAITaskParam.ID, wsWithEmptyAITaskParam.ID)
require.Len(t, res.Workspaces, 3)
workspaceIDs = []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID, res.Workspaces[2].ID}
require.Contains(t, workspaceIDs, wsWithoutAITask.ID)
require.Contains(t, workspaceIDs, wsCompletedWithAITaskParam.ID)
require.Contains(t, workspaceIDs, wsWithEmptyAITaskParam.ID)
// Test no filter returns all // Test no filter returns all
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{}) res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, res.Workspaces, 5) require.Len(t, res.Workspaces, 4)
} }
func TestWorkspaceAppUpsertRestart(t *testing.T) { func TestWorkspaceAppUpsertRestart(t *testing.T) {
+8 -38
View File
@@ -17,46 +17,16 @@ import (
// AITaskPromptParameterName is the name of the parameter used to pass prompts // AITaskPromptParameterName is the name of the parameter used to pass prompts
// to AI tasks. // to AI tasks.
// //
// Experimental: This value is experimental and may change in the future. // Deprecated: This constant is deprecated and maintained only for backwards
// compatibility with older templates. Task prompts are now stored directly
// in the tasks.prompt database column. New code should access prompts via
// the Task.InitialPrompt field returned from task endpoints.
//
// This constant will be removed in a future major version. Templates should
// not rely on this parameter name, as the backend will continue to create it
// automatically for compatibility but reads from tasks.prompt.
const AITaskPromptParameterName = provider.TaskPromptParameterName const AITaskPromptParameterName = provider.TaskPromptParameterName
// AITasksPromptsResponse represents the response from the AITaskPrompts method.
//
// Experimental: This method is experimental and may change in the future.
type AITasksPromptsResponse struct {
// Prompts is a map of workspace build IDs to prompts.
Prompts map[string]string `json:"prompts"`
}
// AITaskPrompts returns prompts for multiple workspace builds by their IDs.
//
// Experimental: This method is experimental and may change in the future.
func (c *ExperimentalClient) AITaskPrompts(ctx context.Context, buildIDs []uuid.UUID) (AITasksPromptsResponse, error) {
if len(buildIDs) == 0 {
return AITasksPromptsResponse{
Prompts: make(map[string]string),
}, nil
}
// Convert UUIDs to strings and join them
buildIDStrings := make([]string, len(buildIDs))
for i, id := range buildIDs {
buildIDStrings[i] = id.String()
}
buildIDsParam := strings.Join(buildIDStrings, ",")
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/aitasks/prompts", nil, WithQueryParam("build_ids", buildIDsParam))
if err != nil {
return AITasksPromptsResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return AITasksPromptsResponse{}, ReadBodyAsError(res)
}
var prompts AITasksPromptsResponse
return prompts, json.NewDecoder(res.Body).Decode(&prompts)
}
// CreateTaskRequest represents the request to create a new task. // CreateTaskRequest represents the request to create a new task.
// //
// Experimental: This type is experimental and may change in the future. // Experimental: This type is experimental and may change in the future.
-21
View File
@@ -2661,27 +2661,6 @@ export type CreateTaskFeedbackRequest = {
class ExperimentalApiMethods { class ExperimentalApiMethods {
constructor(protected readonly axios: AxiosInstance) {} constructor(protected readonly axios: AxiosInstance) {}
getAITasksPrompts = async (
buildIds: TypesGen.WorkspaceBuild["id"][],
): Promise<TypesGen.AITasksPromptsResponse> => {
if (buildIds.length === 0) {
return {
prompts: {},
};
}
const response = await this.axios.get<TypesGen.AITasksPromptsResponse>(
"/api/experimental/aitasks/prompts",
{
params: {
build_ids: buildIds.join(","),
},
},
);
return response.data;
};
createTask = async ( createTask = async (
user: string, user: string,
req: TypesGen.CreateTaskRequest, req: TypesGen.CreateTaskRequest,
+8 -14
View File
@@ -108,23 +108,17 @@ export interface AIConfig {
* AITaskPromptParameterName is the name of the parameter used to pass prompts * AITaskPromptParameterName is the name of the parameter used to pass prompts
* to AI tasks. * to AI tasks.
* *
* Experimental: This value is experimental and may change in the future. * Deprecated: This constant is deprecated and maintained only for backwards
* compatibility with older templates. Task prompts are now stored directly
* in the tasks.prompt database column. New code should access prompts via
* the Task.InitialPrompt field returned from task endpoints.
*
* This constant will be removed in a future major version. Templates should
* not rely on this parameter name, as the backend will continue to create it
* automatically for compatibility but reads from tasks.prompt.
*/ */
export const AITaskPromptParameterName = "AI Prompt"; export const AITaskPromptParameterName = "AI Prompt";
// From codersdk/aitasks.go
/**
* AITasksPromptsResponse represents the response from the AITaskPrompts method.
*
* Experimental: This method is experimental and may change in the future.
*/
export interface AITasksPromptsResponse {
/**
* Prompts is a map of workspace build IDs to prompts.
*/
readonly prompts: Record<string, string>;
}
// From codersdk/allowlist.go // From codersdk/allowlist.go
/** /**
* APIAllowListTarget represents a single allow-list entry using the canonical * APIAllowListTarget represents a single allow-list entry using the canonical
@@ -7,6 +7,7 @@ import type {
Template, Template,
TemplateVersionExternalAuth, TemplateVersionExternalAuth,
} from "api/typesGenerated"; } from "api/typesGenerated";
import { AITaskPromptParameterName } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Button } from "components/Button/Button"; import { Button } from "components/Button/Button";
import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { ExternalImage } from "components/ExternalImage/ExternalImage";
@@ -38,8 +39,6 @@ import { docs } from "utils/docs";
import { PromptSelectTrigger } from "./PromptSelectTrigger"; import { PromptSelectTrigger } from "./PromptSelectTrigger";
import { TemplateVersionSelect } from "./TemplateVersionSelect"; import { TemplateVersionSelect } from "./TemplateVersionSelect";
const AI_PROMPT_PARAMETER_NAME = "AI Prompt";
type TaskPromptProps = { type TaskPromptProps = {
templates: Template[] | undefined; templates: Template[] | undefined;
error: unknown; error: unknown;
@@ -168,7 +167,7 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
// Read-only prompt if defined in preset // Read-only prompt if defined in preset
const presetPrompt = selectedPreset?.Parameters?.find( const presetPrompt = selectedPreset?.Parameters?.find(
(param) => param.Name === AI_PROMPT_PARAMETER_NAME, (param) => param.Name === AITaskPromptParameterName,
)?.Value; )?.Value;
const isPromptReadOnly = !!presetPrompt; const isPromptReadOnly = !!presetPrompt;
useEffect(() => { useEffect(() => {