mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add display name field for tasks (#20856)
## Problem Tasks currently only expose a machine-friendly name field (e.g. `task-python-debug-a1b2`), but this value is primarily an identifier rather than a clean, descriptive label. We need a separate display-friendly name for use in the UI. This PR introduces a new `display_name` field and updates the task-name generation flow. The Claude system prompt was updated to return valid JSON with both `name` and `display_name`. The name generation logic follows a fallback chain (Anthropic > prompt sanitization > random fallback). To make task names more closely resemble their display names, the legacy `task-` prefix has been removed. For context, PR https://github.com/coder/coder/pull/20834 introduced a small Task icon to the workspace list to help identify workspaces associated to tasks. ## Changes - Database migration: Added `display_name` column to tasks table - Updated system prompt to generate both task name and display name as valid JSON - Task name generation now follows a fallback chain: Anthropic > prompt sanitization > random fallback - Removed `task-` prefix from task names to allow more descriptive names - Note: PR https://github.com/coder/coder/pull/20834 adds a Task icon to workspaces in the workspace list to distinguish task-created workspaces **Note:** UI changes will be addressed in a follow-up PR Related to: https://github.com/coder/coder/issues/20801
This commit is contained in:
@@ -189,6 +189,7 @@ func Test_TaskStatus(t *testing.T) {
|
||||
"owner_id": "00000000-0000-0000-0000-000000000000",
|
||||
"owner_name": "me",
|
||||
"name": "exists",
|
||||
"display_name": "Task exists",
|
||||
"template_id": "00000000-0000-0000-0000-000000000000",
|
||||
"template_version_id": "00000000-0000-0000-0000-000000000000",
|
||||
"template_name": "",
|
||||
@@ -220,9 +221,10 @@ func Test_TaskStatus(t *testing.T) {
|
||||
switch r.URL.Path {
|
||||
case "/api/experimental/tasks/me/exists":
|
||||
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
|
||||
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
||||
Name: "exists",
|
||||
OwnerName: "me",
|
||||
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
||||
Name: "exists",
|
||||
DisplayName: "Task exists",
|
||||
OwnerName: "me",
|
||||
WorkspaceAgentHealth: &codersdk.WorkspaceAgentHealth{
|
||||
Healthy: true,
|
||||
},
|
||||
|
||||
+21
-14
@@ -13,8 +13,9 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/coderd/taskname"
|
||||
|
||||
aiagentapi "github.com/coder/agentapi-sdk-go"
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
@@ -24,12 +25,9 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/searchquery"
|
||||
"github.com/coder/coder/v2/coderd/taskname"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
|
||||
aiagentapi "github.com/coder/agentapi-sdk-go"
|
||||
)
|
||||
|
||||
// @Summary Create a new AI task
|
||||
@@ -111,18 +109,25 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if taskName == "" {
|
||||
taskName = taskname.GenerateFallback()
|
||||
taskDisplayName := strings.TrimSpace(req.DisplayName)
|
||||
if taskDisplayName != "" {
|
||||
if len(taskDisplayName) > 64 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Display name must be 64 characters or less.",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if anthropicAPIKey := taskname.GetAnthropicAPIKeyFromEnv(); anthropicAPIKey != "" {
|
||||
anthropicModel := taskname.GetAnthropicModelFromEnv()
|
||||
// Generate task name and display name if either is not provided
|
||||
if taskName == "" || taskDisplayName == "" {
|
||||
generatedTaskName := taskname.Generate(ctx, api.Logger, req.Input)
|
||||
|
||||
generatedName, err := taskname.Generate(ctx, req.Input, 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 = generatedTaskName.Name
|
||||
}
|
||||
if taskDisplayName == "" {
|
||||
taskDisplayName = generatedTaskName.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +220,7 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
|
||||
OrganizationID: templateVersion.OrganizationID,
|
||||
OwnerID: owner.ID,
|
||||
Name: taskName,
|
||||
DisplayName: taskDisplayName,
|
||||
WorkspaceID: uuid.NullUUID{}, // Will be set after workspace creation.
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
TemplateParameters: []byte("{}"),
|
||||
@@ -304,6 +310,7 @@ func taskFromDBTaskAndWorkspace(dbTask database.Task, ws codersdk.Workspace) cod
|
||||
OwnerName: dbTask.OwnerUsername,
|
||||
OwnerAvatarURL: dbTask.OwnerAvatarUrl,
|
||||
Name: dbTask.Name,
|
||||
DisplayName: dbTask.DisplayName,
|
||||
TemplateID: ws.TemplateID,
|
||||
TemplateVersionID: dbTask.TemplateVersionID,
|
||||
TemplateName: ws.TemplateName,
|
||||
|
||||
+49
-7
@@ -1049,14 +1049,17 @@ func TestTasksCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
taskName string
|
||||
expectFallbackName bool
|
||||
expectError string
|
||||
name string
|
||||
taskName string
|
||||
taskDisplayName string
|
||||
expectFallbackName bool
|
||||
expectFallbackDisplayName bool
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "ValidName",
|
||||
taskName: "a-valid-task-name",
|
||||
name: "ValidName",
|
||||
taskName: "a-valid-task-name",
|
||||
expectFallbackDisplayName: true,
|
||||
},
|
||||
{
|
||||
name: "NotValidName",
|
||||
@@ -1066,8 +1069,37 @@ func TestTasksCreate(t *testing.T) {
|
||||
{
|
||||
name: "NoNameProvided",
|
||||
taskName: "",
|
||||
taskDisplayName: "A valid task display name",
|
||||
expectFallbackName: true,
|
||||
},
|
||||
{
|
||||
name: "ValidDisplayName",
|
||||
taskDisplayName: "A valid task display name",
|
||||
expectFallbackName: true,
|
||||
},
|
||||
{
|
||||
name: "NotValidDisplayName",
|
||||
taskDisplayName: "This is a task display name with a length greater than 64 characters.",
|
||||
expectError: "Display name must be 64 characters or less.",
|
||||
},
|
||||
{
|
||||
name: "NoDisplayNameProvided",
|
||||
taskName: "a-valid-task-name",
|
||||
taskDisplayName: "",
|
||||
expectFallbackDisplayName: true,
|
||||
},
|
||||
{
|
||||
name: "ValidNameAndDisplayName",
|
||||
taskName: "a-valid-task-name",
|
||||
taskDisplayName: "A valid task display name",
|
||||
},
|
||||
{
|
||||
name: "NoNameAndDisplayNameProvided",
|
||||
taskName: "",
|
||||
taskDisplayName: "",
|
||||
expectFallbackName: true,
|
||||
expectFallbackDisplayName: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -1098,6 +1130,7 @@ func TestTasksCreate(t *testing.T) {
|
||||
TemplateVersionID: template.ActiveVersionID,
|
||||
Input: "Some prompt",
|
||||
Name: tt.taskName,
|
||||
DisplayName: tt.taskDisplayName,
|
||||
})
|
||||
if tt.expectError == "" {
|
||||
require.NoError(t, err)
|
||||
@@ -1111,8 +1144,17 @@ func TestTasksCreate(t *testing.T) {
|
||||
if !tt.expectFallbackName {
|
||||
require.Equal(t, tt.taskName, task.Name)
|
||||
}
|
||||
|
||||
// Then: We expect the correct display name to have been picked.
|
||||
require.NotEmpty(t, task.DisplayName)
|
||||
if !tt.expectFallbackDisplayName {
|
||||
require.Equal(t, tt.taskDisplayName, task.DisplayName)
|
||||
}
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.expectError)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
require.Equal(t, apiErr.Message, tt.expectError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Generated
+6
@@ -13277,6 +13277,9 @@ const docTemplate = `{
|
||||
"codersdk.CreateTaskRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"input": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -17876,6 +17879,9 @@ const docTemplate = `{
|
||||
"current_state": {
|
||||
"$ref": "#/definitions/codersdk.TaskStateEntry"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
||||
Generated
+6
@@ -11921,6 +11921,9 @@
|
||||
"codersdk.CreateTaskRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"input": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -16358,6 +16361,9 @@
|
||||
"current_state": {
|
||||
"$ref": "#/definitions/codersdk.TaskStateEntry"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -1582,11 +1584,13 @@ func Task(t testing.TB, db database.Store, orig database.TaskTable) database.Tas
|
||||
parameters = json.RawMessage([]byte("{}"))
|
||||
}
|
||||
|
||||
taskName := taskname.Generate(genCtx, slog.Make(), orig.Prompt)
|
||||
task, err := db.InsertTask(genCtx, database.InsertTaskParams{
|
||||
ID: takeFirst(orig.ID, uuid.New()),
|
||||
OrganizationID: orig.OrganizationID,
|
||||
OwnerID: orig.OwnerID,
|
||||
Name: takeFirst(orig.Name, taskname.GenerateFallback()),
|
||||
Name: takeFirst(orig.Name, taskName.Name),
|
||||
DisplayName: takeFirst(orig.DisplayName, taskName.DisplayName),
|
||||
WorkspaceID: orig.WorkspaceID,
|
||||
TemplateVersionID: orig.TemplateVersionID,
|
||||
TemplateParameters: parameters,
|
||||
|
||||
Generated
+5
-1
@@ -1826,9 +1826,12 @@ CREATE TABLE tasks (
|
||||
template_parameters jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
prompt text NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
deleted_at timestamp with time zone
|
||||
deleted_at timestamp with time zone,
|
||||
display_name character varying(127) DEFAULT ''::character varying NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN tasks.display_name IS 'Display name is a custom, human-friendly task name.';
|
||||
|
||||
CREATE VIEW visible_users AS
|
||||
SELECT users.id,
|
||||
users.username,
|
||||
@@ -1964,6 +1967,7 @@ CREATE VIEW tasks_with_status AS
|
||||
tasks.prompt,
|
||||
tasks.created_at,
|
||||
tasks.deleted_at,
|
||||
tasks.display_name,
|
||||
CASE
|
||||
WHEN (tasks.workspace_id IS NULL) THEN 'pending'::task_status
|
||||
WHEN (build_status.status <> 'active'::task_status) THEN build_status.status
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
-- Drop view first before removing the display_name column from tasks
|
||||
DROP VIEW IF EXISTS tasks_with_status;
|
||||
|
||||
-- Remove display_name column from tasks
|
||||
ALTER TABLE tasks DROP COLUMN display_name;
|
||||
|
||||
-- Recreate view without the display_name column.
|
||||
-- This restores the view to its previous state after removing display_name from tasks.
|
||||
CREATE VIEW
|
||||
tasks_with_status
|
||||
AS
|
||||
SELECT
|
||||
tasks.*,
|
||||
CASE
|
||||
WHEN tasks.workspace_id IS NULL OR latest_build.job_status IS NULL THEN 'pending'::task_status
|
||||
|
||||
WHEN latest_build.job_status = 'failed' THEN 'error'::task_status
|
||||
|
||||
WHEN latest_build.transition IN ('stop', 'delete')
|
||||
AND latest_build.job_status = 'succeeded' THEN 'paused'::task_status
|
||||
|
||||
WHEN latest_build.transition = 'start'
|
||||
AND latest_build.job_status = 'pending' THEN 'initializing'::task_status
|
||||
|
||||
WHEN latest_build.transition = 'start' AND latest_build.job_status IN ('running', 'succeeded') THEN
|
||||
CASE
|
||||
WHEN agent_status.none THEN 'initializing'::task_status
|
||||
WHEN agent_status.connecting THEN 'initializing'::task_status
|
||||
WHEN agent_status.connected THEN
|
||||
CASE
|
||||
WHEN app_status.any_unhealthy THEN 'error'::task_status
|
||||
WHEN app_status.any_initializing THEN 'initializing'::task_status
|
||||
WHEN app_status.all_healthy_or_disabled THEN 'active'::task_status
|
||||
ELSE 'unknown'::task_status
|
||||
END
|
||||
ELSE 'unknown'::task_status
|
||||
END
|
||||
|
||||
ELSE 'unknown'::task_status
|
||||
END AS status,
|
||||
task_app.*,
|
||||
task_owner.*
|
||||
FROM
|
||||
tasks
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT
|
||||
vu.username AS owner_username,
|
||||
vu.name AS owner_name,
|
||||
vu.avatar_url AS owner_avatar_url
|
||||
FROM visible_users vu
|
||||
WHERE vu.id = tasks.owner_id
|
||||
) task_owner
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT workspace_build_number, workspace_agent_id, workspace_app_id
|
||||
FROM task_workspace_apps task_app
|
||||
WHERE task_id = tasks.id
|
||||
ORDER BY workspace_build_number DESC
|
||||
LIMIT 1
|
||||
) task_app ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
workspace_build.transition,
|
||||
provisioner_job.job_status,
|
||||
workspace_build.job_id
|
||||
FROM workspace_builds workspace_build
|
||||
JOIN provisioner_jobs provisioner_job ON provisioner_job.id = workspace_build.job_id
|
||||
WHERE workspace_build.workspace_id = tasks.workspace_id
|
||||
AND workspace_build.build_number = task_app.workspace_build_number
|
||||
) latest_build ON TRUE
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*) = 0 AS none,
|
||||
bool_or(workspace_agent.lifecycle_state IN ('created', 'starting')) AS connecting,
|
||||
bool_and(workspace_agent.lifecycle_state = 'ready') AS connected
|
||||
FROM workspace_agents workspace_agent
|
||||
WHERE workspace_agent.id = task_app.workspace_agent_id
|
||||
) agent_status
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT
|
||||
bool_or(workspace_app.health = 'unhealthy') AS any_unhealthy,
|
||||
bool_or(workspace_app.health = 'initializing') AS any_initializing,
|
||||
bool_and(workspace_app.health IN ('healthy', 'disabled')) AS all_healthy_or_disabled
|
||||
FROM workspace_apps workspace_app
|
||||
WHERE workspace_app.id = task_app.workspace_app_id
|
||||
) app_status
|
||||
WHERE
|
||||
tasks.deleted_at IS NULL;
|
||||
@@ -0,0 +1,158 @@
|
||||
-- Add display_name column to tasks table
|
||||
ALTER TABLE tasks ADD COLUMN display_name VARCHAR(127) NOT NULL DEFAULT '';
|
||||
COMMENT ON COLUMN tasks.display_name IS 'Display name is a custom, human-friendly task name.';
|
||||
|
||||
-- Backfill existing tasks with truncated prompt as display name
|
||||
-- Replace newlines/tabs with spaces, truncate to 64 characters and add ellipsis if truncated
|
||||
UPDATE tasks
|
||||
SET display_name = CASE
|
||||
WHEN LENGTH(REGEXP_REPLACE(prompt, E'[\\n\\r\\t]+', ' ', 'g')) > 64
|
||||
THEN LEFT(REGEXP_REPLACE(prompt, E'[\\n\\r\\t]+', ' ', 'g'), 63) || '…'
|
||||
ELSE REGEXP_REPLACE(prompt, E'[\\n\\r\\t]+', ' ', 'g')
|
||||
END
|
||||
WHERE display_name = '';
|
||||
|
||||
-- Recreate the tasks_with_status view to pick up the new display_name column.
|
||||
-- PostgreSQL resolves the tasks.* wildcard when the view is created, not when
|
||||
-- it's queried, so the view must be recreated after adding columns to tasks.
|
||||
DROP VIEW IF EXISTS tasks_with_status;
|
||||
|
||||
CREATE VIEW
|
||||
tasks_with_status
|
||||
AS
|
||||
SELECT
|
||||
tasks.*,
|
||||
-- Combine component statuses with precedence: build -> agent -> app.
|
||||
CASE
|
||||
WHEN tasks.workspace_id IS NULL THEN 'pending'::task_status
|
||||
WHEN build_status.status != 'active' THEN build_status.status::task_status
|
||||
WHEN agent_status.status != 'active' THEN agent_status.status::task_status
|
||||
ELSE app_status.status::task_status
|
||||
END AS status,
|
||||
-- Attach debug information for troubleshooting status.
|
||||
jsonb_build_object(
|
||||
'build', jsonb_build_object(
|
||||
'transition', latest_build_raw.transition,
|
||||
'job_status', latest_build_raw.job_status,
|
||||
'computed', build_status.status
|
||||
),
|
||||
'agent', jsonb_build_object(
|
||||
'lifecycle_state', agent_raw.lifecycle_state,
|
||||
'computed', agent_status.status
|
||||
),
|
||||
'app', jsonb_build_object(
|
||||
'health', app_raw.health,
|
||||
'computed', app_status.status
|
||||
)
|
||||
) AS status_debug,
|
||||
task_app.*,
|
||||
agent_raw.lifecycle_state AS workspace_agent_lifecycle_state,
|
||||
app_raw.health AS workspace_app_health,
|
||||
task_owner.*
|
||||
FROM
|
||||
tasks
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT
|
||||
vu.username AS owner_username,
|
||||
vu.name AS owner_name,
|
||||
vu.avatar_url AS owner_avatar_url
|
||||
FROM
|
||||
visible_users vu
|
||||
WHERE
|
||||
vu.id = tasks.owner_id
|
||||
) task_owner
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
task_app.workspace_build_number,
|
||||
task_app.workspace_agent_id,
|
||||
task_app.workspace_app_id
|
||||
FROM
|
||||
task_workspace_apps task_app
|
||||
WHERE
|
||||
task_id = tasks.id
|
||||
ORDER BY
|
||||
task_app.workspace_build_number DESC
|
||||
LIMIT 1
|
||||
) task_app ON TRUE
|
||||
|
||||
-- Join the raw data for computing task status.
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
workspace_build.transition,
|
||||
provisioner_job.job_status,
|
||||
workspace_build.job_id
|
||||
FROM
|
||||
workspace_builds workspace_build
|
||||
JOIN
|
||||
provisioner_jobs provisioner_job
|
||||
ON provisioner_job.id = workspace_build.job_id
|
||||
WHERE
|
||||
workspace_build.workspace_id = tasks.workspace_id
|
||||
AND workspace_build.build_number = task_app.workspace_build_number
|
||||
) latest_build_raw ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
workspace_agent.lifecycle_state
|
||||
FROM
|
||||
workspace_agents workspace_agent
|
||||
WHERE
|
||||
workspace_agent.id = task_app.workspace_agent_id
|
||||
) agent_raw ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
workspace_app.health
|
||||
FROM
|
||||
workspace_apps workspace_app
|
||||
WHERE
|
||||
workspace_app.id = task_app.workspace_app_id
|
||||
) app_raw ON TRUE
|
||||
|
||||
-- Compute the status for each component.
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN latest_build_raw.job_status IS NULL THEN 'pending'::task_status
|
||||
WHEN latest_build_raw.job_status IN ('failed', 'canceling', 'canceled') THEN 'error'::task_status
|
||||
WHEN
|
||||
latest_build_raw.transition IN ('stop', 'delete')
|
||||
AND latest_build_raw.job_status = 'succeeded' THEN 'paused'::task_status
|
||||
WHEN
|
||||
latest_build_raw.transition = 'start'
|
||||
AND latest_build_raw.job_status = 'pending' THEN 'initializing'::task_status
|
||||
-- Build is running or done, defer to agent/app status.
|
||||
WHEN
|
||||
latest_build_raw.transition = 'start'
|
||||
AND latest_build_raw.job_status IN ('running', 'succeeded') THEN 'active'::task_status
|
||||
ELSE 'unknown'::task_status
|
||||
END AS status
|
||||
) build_status
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT
|
||||
CASE
|
||||
-- No agent or connecting.
|
||||
WHEN
|
||||
agent_raw.lifecycle_state IS NULL
|
||||
OR agent_raw.lifecycle_state IN ('created', 'starting') THEN 'initializing'::task_status
|
||||
-- Agent is running, defer to app status.
|
||||
-- NOTE(mafredri): The start_error/start_timeout states means connected, but some startup script failed.
|
||||
-- This may or may not affect the task status but this has to be caught by app health check.
|
||||
WHEN agent_raw.lifecycle_state IN ('ready', 'start_timeout', 'start_error') THEN 'active'::task_status
|
||||
-- If the agent is shutting down or turned off, this is an unknown state because we would expect a stop
|
||||
-- build to be running.
|
||||
-- This is essentially equal to: `IN ('shutting_down', 'shutdown_timeout', 'shutdown_error', 'off')`,
|
||||
-- but we cannot use them because the values were added in a migration.
|
||||
WHEN agent_raw.lifecycle_state NOT IN ('created', 'starting', 'ready', 'start_timeout', 'start_error') THEN 'unknown'::task_status
|
||||
ELSE 'unknown'::task_status
|
||||
END AS status
|
||||
) agent_status
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN app_raw.health = 'initializing' THEN 'initializing'::task_status
|
||||
WHEN app_raw.health = 'unhealthy' THEN 'error'::task_status
|
||||
WHEN app_raw.health IN ('healthy', 'disabled') THEN 'active'::task_status
|
||||
ELSE 'unknown'::task_status
|
||||
END AS status
|
||||
) app_status
|
||||
WHERE
|
||||
tasks.deleted_at IS NULL;
|
||||
@@ -143,6 +143,7 @@ func (t Task) TaskTable() TaskTable {
|
||||
OrganizationID: t.OrganizationID,
|
||||
OwnerID: t.OwnerID,
|
||||
Name: t.Name,
|
||||
DisplayName: t.DisplayName,
|
||||
WorkspaceID: t.WorkspaceID,
|
||||
TemplateVersionID: t.TemplateVersionID,
|
||||
TemplateParameters: t.TemplateParameters,
|
||||
|
||||
@@ -4218,6 +4218,7 @@ type Task struct {
|
||||
Prompt string `db:"prompt" json:"prompt"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
Status TaskStatus `db:"status" json:"status"`
|
||||
StatusDebug json.RawMessage `db:"status_debug" json:"status_debug"`
|
||||
WorkspaceBuildNumber sql.NullInt32 `db:"workspace_build_number" json:"workspace_build_number"`
|
||||
@@ -4241,6 +4242,8 @@ type TaskTable struct {
|
||||
Prompt string `db:"prompt" json:"prompt"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"`
|
||||
// Display name is a custom, human-friendly task name.
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
}
|
||||
|
||||
type TaskWorkspaceApp struct {
|
||||
|
||||
@@ -13187,7 +13187,7 @@ SET
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
AND deleted_at IS NULL
|
||||
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at
|
||||
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name
|
||||
`
|
||||
|
||||
type DeleteTaskParams struct {
|
||||
@@ -13209,12 +13209,13 @@ func (q *sqlQuerier) DeleteTask(ctx context.Context, arg DeleteTaskParams) (Task
|
||||
&i.Prompt,
|
||||
&i.CreatedAt,
|
||||
&i.DeletedAt,
|
||||
&i.DisplayName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getTaskByID = `-- name: GetTaskByID :one
|
||||
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status WHERE id = $1::uuid
|
||||
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status WHERE id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetTaskByID(ctx context.Context, id uuid.UUID) (Task, error) {
|
||||
@@ -13231,6 +13232,7 @@ func (q *sqlQuerier) GetTaskByID(ctx context.Context, id uuid.UUID) (Task, error
|
||||
&i.Prompt,
|
||||
&i.CreatedAt,
|
||||
&i.DeletedAt,
|
||||
&i.DisplayName,
|
||||
&i.Status,
|
||||
&i.StatusDebug,
|
||||
&i.WorkspaceBuildNumber,
|
||||
@@ -13246,7 +13248,7 @@ func (q *sqlQuerier) GetTaskByID(ctx context.Context, id uuid.UUID) (Task, error
|
||||
}
|
||||
|
||||
const getTaskByOwnerIDAndName = `-- name: GetTaskByOwnerIDAndName :one
|
||||
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status
|
||||
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status
|
||||
WHERE
|
||||
owner_id = $1::uuid
|
||||
AND deleted_at IS NULL
|
||||
@@ -13272,6 +13274,7 @@ func (q *sqlQuerier) GetTaskByOwnerIDAndName(ctx context.Context, arg GetTaskByO
|
||||
&i.Prompt,
|
||||
&i.CreatedAt,
|
||||
&i.DeletedAt,
|
||||
&i.DisplayName,
|
||||
&i.Status,
|
||||
&i.StatusDebug,
|
||||
&i.WorkspaceBuildNumber,
|
||||
@@ -13287,7 +13290,7 @@ func (q *sqlQuerier) GetTaskByOwnerIDAndName(ctx context.Context, arg GetTaskByO
|
||||
}
|
||||
|
||||
const getTaskByWorkspaceID = `-- name: GetTaskByWorkspaceID :one
|
||||
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status WHERE workspace_id = $1::uuid
|
||||
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status WHERE workspace_id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (Task, error) {
|
||||
@@ -13304,6 +13307,7 @@ func (q *sqlQuerier) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.
|
||||
&i.Prompt,
|
||||
&i.CreatedAt,
|
||||
&i.DeletedAt,
|
||||
&i.DisplayName,
|
||||
&i.Status,
|
||||
&i.StatusDebug,
|
||||
&i.WorkspaceBuildNumber,
|
||||
@@ -13320,10 +13324,10 @@ func (q *sqlQuerier) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.
|
||||
|
||||
const insertTask = `-- name: InsertTask :one
|
||||
INSERT INTO tasks
|
||||
(id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at)
|
||||
(id, organization_id, owner_id, name, display_name, workspace_id, template_version_id, template_parameters, prompt, created_at)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name
|
||||
`
|
||||
|
||||
type InsertTaskParams struct {
|
||||
@@ -13331,6 +13335,7 @@ type InsertTaskParams struct {
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
TemplateParameters json.RawMessage `db:"template_parameters" json:"template_parameters"`
|
||||
@@ -13344,6 +13349,7 @@ func (q *sqlQuerier) InsertTask(ctx context.Context, arg InsertTaskParams) (Task
|
||||
arg.OrganizationID,
|
||||
arg.OwnerID,
|
||||
arg.Name,
|
||||
arg.DisplayName,
|
||||
arg.WorkspaceID,
|
||||
arg.TemplateVersionID,
|
||||
arg.TemplateParameters,
|
||||
@@ -13362,12 +13368,13 @@ func (q *sqlQuerier) InsertTask(ctx context.Context, arg InsertTaskParams) (Task
|
||||
&i.Prompt,
|
||||
&i.CreatedAt,
|
||||
&i.DeletedAt,
|
||||
&i.DisplayName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listTasks = `-- name: ListTasks :many
|
||||
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status tws
|
||||
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status tws
|
||||
WHERE tws.deleted_at IS NULL
|
||||
AND CASE WHEN $1::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.owner_id = $1::UUID ELSE TRUE END
|
||||
AND CASE WHEN $2::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.organization_id = $2::UUID ELSE TRUE END
|
||||
@@ -13401,6 +13408,7 @@ func (q *sqlQuerier) ListTasks(ctx context.Context, arg ListTasksParams) ([]Task
|
||||
&i.Prompt,
|
||||
&i.CreatedAt,
|
||||
&i.DeletedAt,
|
||||
&i.DisplayName,
|
||||
&i.Status,
|
||||
&i.StatusDebug,
|
||||
&i.WorkspaceBuildNumber,
|
||||
@@ -13433,7 +13441,7 @@ SET
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
AND deleted_at IS NULL
|
||||
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at
|
||||
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name
|
||||
`
|
||||
|
||||
type UpdateTaskPromptParams struct {
|
||||
@@ -13455,6 +13463,7 @@ func (q *sqlQuerier) UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptP
|
||||
&i.Prompt,
|
||||
&i.CreatedAt,
|
||||
&i.DeletedAt,
|
||||
&i.DisplayName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -13476,7 +13485,7 @@ WHERE
|
||||
AND w.id = $2
|
||||
AND tv.id = tasks.template_version_id
|
||||
RETURNING
|
||||
tasks.id, tasks.organization_id, tasks.owner_id, tasks.name, tasks.workspace_id, tasks.template_version_id, tasks.template_parameters, tasks.prompt, tasks.created_at, tasks.deleted_at
|
||||
tasks.id, tasks.organization_id, tasks.owner_id, tasks.name, tasks.workspace_id, tasks.template_version_id, tasks.template_parameters, tasks.prompt, tasks.created_at, tasks.deleted_at, tasks.display_name
|
||||
`
|
||||
|
||||
type UpdateTaskWorkspaceIDParams struct {
|
||||
@@ -13498,6 +13507,7 @@ func (q *sqlQuerier) UpdateTaskWorkspaceID(ctx context.Context, arg UpdateTaskWo
|
||||
&i.Prompt,
|
||||
&i.CreatedAt,
|
||||
&i.DeletedAt,
|
||||
&i.DisplayName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
-- name: InsertTask :one
|
||||
INSERT INTO tasks
|
||||
(id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at)
|
||||
(id, organization_id, owner_id, name, display_name, workspace_id, template_version_id, template_parameters, prompt, created_at)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateTaskWorkspaceID :one
|
||||
|
||||
+202
-61
@@ -2,39 +2,82 @@ package taskname
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/anthropics/anthropic-sdk-go"
|
||||
anthropicoption "github.com/anthropics/anthropic-sdk-go/option"
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/aisdk-go"
|
||||
strutil "github.com/coder/coder/v2/coderd/util/strings"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultModel = anthropic.ModelClaude3_5HaikuLatest
|
||||
systemPrompt = `Generate a short workspace name from this AI task prompt.
|
||||
systemPrompt = `Generate a short task display name and name from this AI task prompt.
|
||||
Identify the main task (the core action and subject) and base both names on it.
|
||||
The task display name and name should be as similar as possible so a human can easily associate them.
|
||||
|
||||
Requirements:
|
||||
Requirements for task display name (generate this first):
|
||||
- Human-readable description
|
||||
- Maximum 64 characters total
|
||||
- Should concisely describe the main task
|
||||
|
||||
Requirements for task name:
|
||||
- Should be derived from the display name
|
||||
- Only lowercase letters, numbers, and hyphens
|
||||
- Start with "task-"
|
||||
- No spaces or underscores
|
||||
- Maximum 27 characters total
|
||||
- Descriptive of the main task
|
||||
- Should concisely describe the main task
|
||||
|
||||
Output format (must be valid JSON):
|
||||
{
|
||||
"display_name": "<display_name>",
|
||||
"task_name": "<task_name>"
|
||||
}
|
||||
|
||||
Examples:
|
||||
- "Help me debug a Python script" → "task-python-debug"
|
||||
- "Create a React dashboard component" → "task-react-dashboard"
|
||||
- "Analyze sales data from Q3" → "task-analyze-q3-sales"
|
||||
- "Set up CI/CD pipeline" → "task-setup-cicd"
|
||||
Prompt: "Help me debug a Python script" →
|
||||
{
|
||||
"display_name": "Debug Python script",
|
||||
"task_name": "python-debug"
|
||||
}
|
||||
|
||||
If you cannot create a suitable name:
|
||||
- Respond with "task-unnamed"`
|
||||
Prompt: "Create a React dashboard component" →
|
||||
{
|
||||
"display_name": "React dashboard component",
|
||||
"task_name": "react-dashboard"
|
||||
}
|
||||
|
||||
Prompt: "Analyze sales data from Q3" →
|
||||
{
|
||||
"display_name": "Analyze Q3 sales data",
|
||||
"task_name": "analyze-q3-sales"
|
||||
}
|
||||
|
||||
Prompt: "Set up CI/CD pipeline" →
|
||||
{
|
||||
"display_name": "CI/CD pipeline setup",
|
||||
"task_name": "setup-cicd"
|
||||
}
|
||||
|
||||
If a suitable name cannot be created, output exactly:
|
||||
{
|
||||
"display_name": "Task Unnamed",
|
||||
"task_name": "task-unnamed"
|
||||
}
|
||||
|
||||
Do not include any additional keys, explanations, or text outside the JSON.`
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -42,30 +85,16 @@ var (
|
||||
ErrNoNameGenerated = xerrors.New("no task name generated")
|
||||
)
|
||||
|
||||
type options struct {
|
||||
apiKey string
|
||||
model anthropic.Model
|
||||
type TaskName struct {
|
||||
Name string `json:"task_name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
type Option func(o *options)
|
||||
|
||||
func WithAPIKey(apiKey string) Option {
|
||||
return func(o *options) {
|
||||
o.apiKey = apiKey
|
||||
}
|
||||
}
|
||||
|
||||
func WithModel(model anthropic.Model) Option {
|
||||
return func(o *options) {
|
||||
o.model = model
|
||||
}
|
||||
}
|
||||
|
||||
func GetAnthropicAPIKeyFromEnv() string {
|
||||
func getAnthropicAPIKeyFromEnv() string {
|
||||
return os.Getenv("ANTHROPIC_API_KEY")
|
||||
}
|
||||
|
||||
func GetAnthropicModelFromEnv() anthropic.Model {
|
||||
func getAnthropicModelFromEnv() anthropic.Model {
|
||||
return anthropic.Model(os.Getenv("ANTHROPIC_MODEL"))
|
||||
}
|
||||
|
||||
@@ -79,33 +108,85 @@ func generateSuffix() string {
|
||||
return fmt.Sprintf("%04x", num)
|
||||
}
|
||||
|
||||
func GenerateFallback() string {
|
||||
// generateFallback generates a random task name when other methods fail.
|
||||
// Uses Docker-style name generation with a collision-resistant suffix.
|
||||
func generateFallback() TaskName {
|
||||
// We have a 32 character limit for the name.
|
||||
// We have a 5 character prefix `task-`.
|
||||
// We have a 5 character suffix `-ffff`.
|
||||
// This leaves us with 22 characters for the middle.
|
||||
// This leaves us with 27 characters for the name.
|
||||
//
|
||||
// Unfortunately, `namesgenerator.GetRandomName(0)` will
|
||||
// generate names that are longer than 22 characters, so
|
||||
// we just trim these down to length.
|
||||
// `namesgenerator.GetRandomName(0)` can generate names
|
||||
// up to 27 characters, but we truncate defensively.
|
||||
name := strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-")
|
||||
name = name[:min(len(name), 22)]
|
||||
name = name[:min(len(name), 27)]
|
||||
name = strings.TrimSuffix(name, "-")
|
||||
|
||||
return fmt.Sprintf("task-%s-%s", name, generateSuffix())
|
||||
taskName := fmt.Sprintf("%s-%s", name, generateSuffix())
|
||||
displayName := strings.ReplaceAll(name, "-", " ")
|
||||
if len(displayName) > 0 {
|
||||
displayName = strings.ToUpper(displayName[:1]) + displayName[1:]
|
||||
}
|
||||
|
||||
return TaskName{
|
||||
Name: taskName,
|
||||
DisplayName: displayName,
|
||||
}
|
||||
}
|
||||
|
||||
func Generate(ctx context.Context, prompt string, opts ...Option) (string, error) {
|
||||
o := options{}
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
// generateFromPrompt creates a task name directly from the prompt by sanitizing it.
|
||||
// This is used as a fallback when Claude fails to generate a name.
|
||||
func generateFromPrompt(prompt string) (TaskName, error) {
|
||||
// Normalize newlines and tabs to spaces
|
||||
prompt = regexp.MustCompile(`[\n\r\t]+`).ReplaceAllString(prompt, " ")
|
||||
|
||||
// Truncate prompt to 27 chars with full words for task name generation
|
||||
truncatedForName := prompt
|
||||
if len(prompt) > 27 {
|
||||
truncatedForName = strutil.Truncate(prompt, 27, strutil.TruncateWithFullWords)
|
||||
}
|
||||
|
||||
if o.model == "" {
|
||||
o.model = defaultModel
|
||||
// Generate task name from truncated prompt
|
||||
name := strings.ToLower(truncatedForName)
|
||||
// Replace whitespace (\t \r \n and spaces) sequences with hyphens
|
||||
name = regexp.MustCompile(`\s+`).ReplaceAllString(name, "-")
|
||||
// Remove all characters except lowercase letters, numbers, and hyphens
|
||||
name = regexp.MustCompile(`[^a-z0-9-]+`).ReplaceAllString(name, "")
|
||||
// Collapse multiple consecutive hyphens into a single hyphen
|
||||
name = regexp.MustCompile(`-+`).ReplaceAllString(name, "-")
|
||||
// Remove leading and trailing hyphens
|
||||
name = strings.Trim(name, "-")
|
||||
|
||||
if len(name) == 0 {
|
||||
return TaskName{}, ErrNoNameGenerated
|
||||
}
|
||||
if o.apiKey == "" {
|
||||
return "", ErrNoAPIKey
|
||||
|
||||
taskName := fmt.Sprintf("%s-%s", name, generateSuffix())
|
||||
|
||||
// Use the initial prompt as display name, truncated to 64 chars with full words
|
||||
displayName := strutil.Truncate(prompt, 64, strutil.TruncateWithFullWords, strutil.TruncateWithEllipsis)
|
||||
displayName = strings.TrimSpace(displayName)
|
||||
if len(displayName) == 0 {
|
||||
// Ensure display name is never empty
|
||||
displayName = strings.ReplaceAll(name, "-", " ")
|
||||
}
|
||||
displayName = strings.ToUpper(displayName[:1]) + displayName[1:]
|
||||
|
||||
return TaskName{
|
||||
Name: taskName,
|
||||
DisplayName: displayName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateFromAnthropic uses Claude (Anthropic) to generate semantic task and display names from a user prompt.
|
||||
// It sends the prompt to Claude with a structured system prompt requesting JSON output containing both names.
|
||||
// Returns an error if the API call fails, the response is invalid, or Claude returns an "unnamed" placeholder.
|
||||
func generateFromAnthropic(ctx context.Context, prompt string, apiKey string, model anthropic.Model) (TaskName, error) {
|
||||
anthropicModel := model
|
||||
if anthropicModel == "" {
|
||||
anthropicModel = defaultModel
|
||||
}
|
||||
if apiKey == "" {
|
||||
return TaskName{}, ErrNoAPIKey
|
||||
}
|
||||
|
||||
conversation := []aisdk.Message{
|
||||
@@ -126,42 +207,95 @@ func Generate(ctx context.Context, prompt string, opts ...Option) (string, error
|
||||
}
|
||||
|
||||
anthropicOptions := anthropic.DefaultClientOptions()
|
||||
anthropicOptions = append(anthropicOptions, anthropicoption.WithAPIKey(o.apiKey))
|
||||
anthropicOptions = append(anthropicOptions, anthropicoption.WithAPIKey(apiKey))
|
||||
anthropicClient := anthropic.NewClient(anthropicOptions...)
|
||||
|
||||
stream, err := anthropicDataStream(ctx, anthropicClient, o.model, conversation)
|
||||
stream, err := anthropicDataStream(ctx, anthropicClient, anthropicModel, conversation)
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("create anthropic data stream: %w", err)
|
||||
return TaskName{}, xerrors.Errorf("create anthropic data stream: %w", err)
|
||||
}
|
||||
|
||||
var acc aisdk.DataStreamAccumulator
|
||||
stream = stream.WithAccumulator(&acc)
|
||||
|
||||
if err := stream.Pipe(io.Discard); err != nil {
|
||||
return "", xerrors.Errorf("pipe data stream")
|
||||
return TaskName{}, xerrors.Errorf("pipe data stream")
|
||||
}
|
||||
|
||||
if len(acc.Messages()) == 0 {
|
||||
return "", ErrNoNameGenerated
|
||||
return TaskName{}, ErrNoNameGenerated
|
||||
}
|
||||
|
||||
taskName := acc.Messages()[0].Content
|
||||
if taskName == "task-unnamed" {
|
||||
return "", ErrNoNameGenerated
|
||||
// Parse the JSON response
|
||||
var taskNameResponse TaskName
|
||||
if err := json.Unmarshal([]byte(acc.Messages()[0].Content), &taskNameResponse); err != nil {
|
||||
return TaskName{}, xerrors.Errorf("failed to parse anthropic response: %w", err)
|
||||
}
|
||||
|
||||
taskNameResponse.Name = strings.TrimSpace(taskNameResponse.Name)
|
||||
taskNameResponse.DisplayName = strings.TrimSpace(taskNameResponse.DisplayName)
|
||||
|
||||
if taskNameResponse.Name == "" || taskNameResponse.Name == "task-unnamed" {
|
||||
return TaskName{}, xerrors.Errorf("anthropic returned invalid task name: %q", taskNameResponse.Name)
|
||||
}
|
||||
|
||||
if taskNameResponse.DisplayName == "" || taskNameResponse.DisplayName == "Task Unnamed" {
|
||||
return TaskName{}, xerrors.Errorf("anthropic returned invalid task display name: %q", taskNameResponse.DisplayName)
|
||||
}
|
||||
|
||||
// We append a suffix to the end of the task name to reduce
|
||||
// the chance of collisions. We truncate the task name to
|
||||
// to a maximum of 27 bytes, so that when we append the
|
||||
// a maximum of 27 bytes, so that when we append the
|
||||
// 5 byte suffix (`-` and 4 byte hex slug), it should
|
||||
// remain within the 32 byte workspace name limit.
|
||||
taskName = taskName[:min(len(taskName), 27)]
|
||||
taskName = fmt.Sprintf("%s-%s", taskName, generateSuffix())
|
||||
if err := codersdk.NameValid(taskName); err != nil {
|
||||
return "", xerrors.Errorf("generated name %v not valid: %w", taskName, err)
|
||||
name := taskNameResponse.Name[:min(len(taskNameResponse.Name), 27)]
|
||||
name = strings.TrimSuffix(name, "-")
|
||||
name = fmt.Sprintf("%s-%s", name, generateSuffix())
|
||||
if err := codersdk.NameValid(name); err != nil {
|
||||
return TaskName{}, xerrors.Errorf("generated name %v not valid: %w", name, err)
|
||||
}
|
||||
|
||||
return taskName, nil
|
||||
displayName := taskNameResponse.DisplayName
|
||||
displayName = strings.TrimSpace(displayName)
|
||||
if len(displayName) == 0 {
|
||||
// Ensure display name is never empty
|
||||
displayName = strings.ReplaceAll(taskNameResponse.Name, "-", " ")
|
||||
}
|
||||
displayName = strings.ToUpper(displayName[:1]) + displayName[1:]
|
||||
|
||||
return TaskName{
|
||||
Name: name,
|
||||
DisplayName: displayName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Generate creates a task name and display name from a user prompt.
|
||||
// It attempts multiple strategies in order of preference:
|
||||
// 1. Use Claude (Anthropic) to generate semantic names from the prompt if an API key is available
|
||||
// 2. Sanitize the prompt directly into a valid task name
|
||||
// 3. Generate a random name as a final fallback
|
||||
//
|
||||
// A suffix is always appended to task names to reduce collision risk.
|
||||
// This function always succeeds and returns a valid TaskName.
|
||||
func Generate(ctx context.Context, logger slog.Logger, prompt string) TaskName {
|
||||
if anthropicAPIKey := getAnthropicAPIKeyFromEnv(); anthropicAPIKey != "" {
|
||||
taskName, err := generateFromAnthropic(ctx, prompt, anthropicAPIKey, getAnthropicModelFromEnv())
|
||||
if err == nil {
|
||||
return taskName
|
||||
}
|
||||
// Anthropic failed, fall through to next fallback
|
||||
logger.Error(ctx, "unable to generate task name and display name from Anthropic", slog.Error(err))
|
||||
}
|
||||
|
||||
// Try generating from prompt
|
||||
taskName, err := generateFromPrompt(prompt)
|
||||
if err == nil {
|
||||
return taskName
|
||||
}
|
||||
logger.Warn(ctx, "unable to generate task name and display name from prompt", slog.Error(err))
|
||||
|
||||
// Final fallback
|
||||
return generateFallback()
|
||||
}
|
||||
|
||||
func anthropicDataStream(ctx context.Context, client anthropic.Client, model anthropic.Model, input []aisdk.Message) (aisdk.DataStream, error) {
|
||||
@@ -171,8 +305,15 @@ func anthropicDataStream(ctx context.Context, client anthropic.Client, model ant
|
||||
}
|
||||
|
||||
return aisdk.AnthropicToDataStream(client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{
|
||||
Model: model,
|
||||
MaxTokens: 24,
|
||||
Model: model,
|
||||
// MaxTokens is set to 100 based on the maximum expected output size.
|
||||
// The worst-case JSON output is 134 characters:
|
||||
// - Base structure: 43 chars (including formatting)
|
||||
// - task_name: 27 chars max
|
||||
// - display_name: 64 chars max
|
||||
// Using Anthropic's token counting API, this worst-case output tokenizes to 70 tokens.
|
||||
// We set MaxTokens to 100 to provide a safety buffer.
|
||||
MaxTokens: 100,
|
||||
System: system,
|
||||
Messages: messages,
|
||||
})), nil
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package taskname
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestGenerateFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
taskName := generateFallback()
|
||||
err := codersdk.NameValid(taskName.Name)
|
||||
require.NoErrorf(t, err, "expected fallback to be valid workspace name, instead found %s", taskName.Name)
|
||||
require.NotEmpty(t, taskName.DisplayName)
|
||||
}
|
||||
|
||||
func TestGenerateFromPrompt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prompt string
|
||||
expectError bool
|
||||
expectedName string
|
||||
expectedDisplayName string
|
||||
}{
|
||||
{
|
||||
name: "EmptyPrompt",
|
||||
prompt: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "OnlySpaces",
|
||||
prompt: " ",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "OnlySpecialCharacters",
|
||||
prompt: "!@#$%^&*()",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "UppercasePrompt",
|
||||
prompt: "BUILD MY APP",
|
||||
expectError: false,
|
||||
expectedName: "build-my-app",
|
||||
expectedDisplayName: "BUILD MY APP",
|
||||
},
|
||||
{
|
||||
name: "PromptWithApostrophes",
|
||||
prompt: "fix user's dashboard",
|
||||
expectError: false,
|
||||
expectedName: "fix-users-dashboard",
|
||||
expectedDisplayName: "Fix user's dashboard",
|
||||
},
|
||||
{
|
||||
name: "LongPrompt",
|
||||
prompt: strings.Repeat("a", 100),
|
||||
expectError: false,
|
||||
expectedName: strings.Repeat("a", 27),
|
||||
expectedDisplayName: "A" + strings.Repeat("a", 62) + "…",
|
||||
},
|
||||
{
|
||||
name: "PromptWithMultipleSpaces",
|
||||
prompt: "build my app",
|
||||
expectError: false,
|
||||
expectedName: "build-my-app",
|
||||
expectedDisplayName: "Build my app",
|
||||
},
|
||||
{
|
||||
name: "PromptWithNewlines",
|
||||
prompt: "build\nmy\napp",
|
||||
expectError: false,
|
||||
expectedName: "build-my-app",
|
||||
expectedDisplayName: "Build my app",
|
||||
},
|
||||
{
|
||||
name: "TruncatesLongPromptAtWordBoundary",
|
||||
prompt: "implement real-time notifications dashboard",
|
||||
expectError: false,
|
||||
expectedName: "implement-real-time",
|
||||
expectedDisplayName: "Implement real-time notifications dashboard",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
taskName, err := generateFromPrompt(tc.prompt)
|
||||
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// Validate task name
|
||||
require.Contains(t, taskName.Name, fmt.Sprintf("%s-", tc.expectedName))
|
||||
require.NoError(t, codersdk.NameValid(taskName.Name))
|
||||
|
||||
// Validate task display name
|
||||
require.NotEmpty(t, taskName.DisplayName)
|
||||
require.Equal(t, tc.expectedDisplayName, taskName.DisplayName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateFromAnthropic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
apiKey := getAnthropicAPIKeyFromEnv()
|
||||
if apiKey == "" {
|
||||
t.Skip("Skipping test as ANTHROPIC_API_KEY not set")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prompt string
|
||||
}{
|
||||
{
|
||||
name: "SimplePrompt",
|
||||
prompt: "Create a finance planning app",
|
||||
},
|
||||
{
|
||||
name: "TechnicalPrompt",
|
||||
prompt: "Debug authentication middleware for OAuth2",
|
||||
},
|
||||
{
|
||||
name: "ShortPrompt",
|
||||
prompt: "Fix bug",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
taskName, err := generateFromAnthropic(ctx, tc.prompt, apiKey, getAnthropicModelFromEnv())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("Task name:", taskName.Name)
|
||||
t.Log("Task display name:", taskName.DisplayName)
|
||||
|
||||
// Validate task name
|
||||
require.NotEmpty(t, taskName.DisplayName)
|
||||
require.NoError(t, codersdk.NameValid(taskName.Name))
|
||||
|
||||
// Validate display name
|
||||
require.NotEmpty(t, taskName.DisplayName)
|
||||
require.NotEqual(t, "task-unnamed", taskName.Name)
|
||||
require.NotEqual(t, "Task Unnamed", taskName.DisplayName)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,42 +15,51 @@ const (
|
||||
anthropicEnvVar = "ANTHROPIC_API_KEY"
|
||||
)
|
||||
|
||||
func TestGenerateFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
name := taskname.GenerateFallback()
|
||||
err := codersdk.NameValid(name)
|
||||
require.NoErrorf(t, err, "expected fallback to be valid workspace name, instead found %s", name)
|
||||
}
|
||||
|
||||
func TestGenerateTaskName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Fallback", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
func TestGenerate(t *testing.T) {
|
||||
t.Run("FromPrompt", func(t *testing.T) {
|
||||
// Ensure no API key in env for this test
|
||||
t.Setenv("ANTHROPIC_API_KEY", "")
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
name, err := taskname.Generate(ctx, "Some random prompt")
|
||||
require.ErrorIs(t, err, taskname.ErrNoAPIKey)
|
||||
require.Equal(t, "", name)
|
||||
taskName := taskname.Generate(ctx, testutil.Logger(t), "Create a finance planning app")
|
||||
|
||||
// Should succeed via prompt sanitization
|
||||
require.NoError(t, codersdk.NameValid(taskName.Name))
|
||||
require.Contains(t, taskName.Name, "create-a-finance-planning-")
|
||||
require.NotEmpty(t, taskName.DisplayName)
|
||||
require.Equal(t, "Create a finance planning app", taskName.DisplayName)
|
||||
})
|
||||
|
||||
t.Run("Anthropic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("FromAnthropic", func(t *testing.T) {
|
||||
apiKey := os.Getenv(anthropicEnvVar)
|
||||
if apiKey == "" {
|
||||
t.Skipf("Skipping test as %s not set", anthropicEnvVar)
|
||||
}
|
||||
|
||||
// Set API key for this test
|
||||
t.Setenv("ANTHROPIC_API_KEY", apiKey)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
name, err := taskname.Generate(ctx, "Create a finance planning app", taskname.WithAPIKey(apiKey))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, "", name)
|
||||
taskName := taskname.Generate(ctx, testutil.Logger(t), "Create a finance planning app")
|
||||
|
||||
err = codersdk.NameValid(name)
|
||||
require.NoError(t, err, "name should be valid")
|
||||
// Should succeed with Claude-generated names
|
||||
require.NoError(t, codersdk.NameValid(taskName.Name))
|
||||
require.NotEmpty(t, taskName.DisplayName)
|
||||
})
|
||||
|
||||
t.Run("Fallback", func(t *testing.T) {
|
||||
// Ensure no API key
|
||||
t.Setenv("ANTHROPIC_API_KEY", "")
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
// Use a prompt that can't be sanitized (only special chars)
|
||||
taskName := taskname.Generate(ctx, testutil.Logger(t), "!@#$%^&*()")
|
||||
|
||||
// Should fall back to random name
|
||||
require.NoError(t, codersdk.NameValid(taskName.Name))
|
||||
require.NotEmpty(t, taskName.DisplayName)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ type CreateTaskRequest struct {
|
||||
TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"`
|
||||
Input string `json:"input"`
|
||||
Name string `json:"name,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
}
|
||||
|
||||
// CreateTask creates a new task.
|
||||
@@ -128,6 +129,7 @@ type Task struct {
|
||||
OwnerName string `json:"owner_name" table:"owner name"`
|
||||
OwnerAvatarURL string `json:"owner_avatar_url,omitempty" table:"owner avatar url"`
|
||||
Name string `json:"name" table:"name,default_sort"`
|
||||
DisplayName string `json:"display_name" table:"display_name"`
|
||||
TemplateID uuid.UUID `json:"template_id" format:"uuid" table:"template id"`
|
||||
TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid" table:"template version id"`
|
||||
TemplateName string `json:"template_name" table:"template name"`
|
||||
|
||||
@@ -32,7 +32,7 @@ We track the following resources:
|
||||
| OrganizationSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>assign_default</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
|
||||
| PrebuildsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>reconciliation_paused</td><td>true</td></tr></tbody></table> |
|
||||
| RoleSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
|
||||
| TaskTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted_at</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>prompt</td><td>true</td></tr><tr><td>template_parameters</td><td>true</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>workspace_id</td><td>true</td></tr></tbody></table> |
|
||||
| TaskTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>prompt</td><td>true</td></tr><tr><td>template_parameters</td><td>true</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>workspace_id</td><td>true</td></tr></tbody></table> |
|
||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>cors_behavior</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_classic_parameter_flow</td><td>true</td></tr><tr><td>use_terraform_workspace_cache</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_system</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
|
||||
Generated
+1
@@ -49,6 +49,7 @@ curl -X POST http://coder-server:8080/api/v2/api/experimental/tasks/{user} \
|
||||
|
||||
```json
|
||||
{
|
||||
"display_name": "string",
|
||||
"input": "string",
|
||||
"name": "string",
|
||||
"template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1",
|
||||
|
||||
Generated
+5
@@ -2085,6 +2085,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
|
||||
```json
|
||||
{
|
||||
"display_name": "string",
|
||||
"input": "string",
|
||||
"name": "string",
|
||||
"template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1",
|
||||
@@ -2096,6 +2097,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|------------------------------|--------|----------|--------------|-------------|
|
||||
| `display_name` | string | false | | |
|
||||
| `input` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `template_version_id` | string | false | | |
|
||||
@@ -7805,6 +7807,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
"timestamp": "2019-08-24T14:15:22Z",
|
||||
"uri": "string"
|
||||
},
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initial_prompt": "string",
|
||||
"name": "string",
|
||||
@@ -7848,6 +7851,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
|-----------------------------|----------------------------------------------------------------------|----------|--------------|-------------|
|
||||
| `created_at` | string | false | | |
|
||||
| `current_state` | [codersdk.TaskStateEntry](#codersdktaskstateentry) | false | | |
|
||||
| `display_name` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `initial_prompt` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
@@ -8032,6 +8036,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
"timestamp": "2019-08-24T14:15:22Z",
|
||||
"uri": "string"
|
||||
},
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initial_prompt": "string",
|
||||
"name": "string",
|
||||
|
||||
@@ -353,6 +353,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||
"organization_id": ActionIgnore, // Never changes.
|
||||
"owner_id": ActionTrack,
|
||||
"name": ActionTrack,
|
||||
"display_name": ActionTrack,
|
||||
"workspace_id": ActionTrack,
|
||||
"template_version_id": ActionTrack,
|
||||
"template_parameters": ActionTrack,
|
||||
|
||||
Generated
+2
@@ -1203,6 +1203,7 @@ export interface CreateTaskRequest {
|
||||
readonly template_version_preset_id?: string;
|
||||
readonly input: string;
|
||||
readonly name?: string;
|
||||
readonly display_name?: string;
|
||||
}
|
||||
|
||||
// From codersdk/organizations.go
|
||||
@@ -4732,6 +4733,7 @@ export interface Task {
|
||||
readonly owner_name: string;
|
||||
readonly owner_avatar_url?: string;
|
||||
readonly name: string;
|
||||
readonly display_name: string;
|
||||
readonly template_id: string;
|
||||
readonly template_version_id: string;
|
||||
readonly template_name: string;
|
||||
|
||||
@@ -4997,6 +4997,7 @@ export const MockAIPromptPresets: TypesGen.Preset[] = [
|
||||
export const MockTask = {
|
||||
id: "test-task",
|
||||
name: "task-wild-test-123",
|
||||
display_name: "Task wild test 123",
|
||||
organization_id: MockOrganization.id,
|
||||
owner_id: MockUserOwner.id,
|
||||
owner_name: MockUserOwner.username,
|
||||
|
||||
Reference in New Issue
Block a user