From a4ccb41e7e6a0717fabe88b78ee4f4c67b267523 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 23 Jul 2025 14:43:20 +0000 Subject: [PATCH 01/17] feat: add preset selector in TasksPage --- site/src/pages/TasksPage/TasksPage.tsx | 120 +++++++++++++++++++------ 1 file changed, 92 insertions(+), 28 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index d678098affd17..93be42e8e4879 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -2,7 +2,11 @@ import Skeleton from "@mui/material/Skeleton"; import { API } from "api/api"; import { getErrorDetail, getErrorMessage } from "api/errors"; import { disabledRefetchOptions } from "api/queries/util"; -import type { Template, TemplateVersionExternalAuth } from "api/typesGenerated"; +import type { + Preset, + Template, + TemplateVersionExternalAuth, +} from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; @@ -50,7 +54,7 @@ import { RedoIcon, RotateCcwIcon, SendIcon } from "lucide-react"; import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; -import { type FC, type ReactNode, useState } from "react"; +import { type FC, type ReactNode, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { Link as RouterLink, useNavigate } from "react-router-dom"; @@ -210,7 +214,11 @@ const TaskFormSection: FC<{ ); }; -type CreateTaskMutationFnProps = { prompt: string; templateVersionId: string }; +type CreateTaskMutationFnProps = { + prompt: string; + templateVersionId: string; + presetId: string | null; +}; type TaskFormProps = { templates: Template[]; @@ -223,6 +231,8 @@ const TaskForm: FC = ({ templates, onSuccess }) => { const [selectedTemplateId, setSelectedTemplateId] = useState( templates[0].id, ); + const [presets, setPresets] = useState(null); + const [selectedPresetId, setSelectedPresetId] = useState(null); const selectedTemplate = templates.find( (t) => t.id === selectedTemplateId, ) as Template; @@ -232,6 +242,28 @@ const TaskForm: FC = ({ templates, onSuccess }) => { isPollingExternalAuth, isLoadingExternalAuth, } = useExternalAuth(selectedTemplate.active_version_id); + + // Fetch presets when template changes + const { data: presetsData } = useQuery({ + queryKey: ["template-version-presets", selectedTemplate.active_version_id], + queryFn: () => + API.getTemplateVersionPresets(selectedTemplate.active_version_id), + ...disabledRefetchOptions, + }); + + // Handle preset data changes + useEffect(() => { + if (presetsData) { + setPresets(presetsData); + // Set default preset if available + const defaultPreset = presetsData.find((p: Preset) => p.Default); + if (defaultPreset) { + setSelectedPresetId(defaultPreset.ID); + } else { + setSelectedPresetId(null); + } + } + }, [presetsData]); const missedExternalAuth = externalAuth?.filter( (auth) => !auth.optional && !auth.authenticated, ); @@ -243,8 +275,9 @@ const TaskForm: FC = ({ templates, onSuccess }) => { mutationFn: async ({ prompt, templateVersionId, + presetId, }: CreateTaskMutationFnProps) => - data.createTask(prompt, user.id, templateVersionId), + data.createTask(prompt, user.id, templateVersionId, presetId), onSuccess: async (task) => { await queryClient.invalidateQueries({ queryKey: ["tasks"], @@ -265,6 +298,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { await createTaskMutation.mutateAsync({ prompt, templateVersionId: selectedTemplate.active_version_id, + presetId: selectedPresetId, }); } catch (error) { const message = getErrorMessage(error, "Error creating task"); @@ -297,27 +331,50 @@ const TaskForm: FC = ({ templates, onSuccess }) => { text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm`} />
- +
+ + + {presets && presets.length > 0 && ( + + )} +
{missedExternalAuth && ( @@ -608,13 +665,20 @@ export const data = { prompt: string, userId: string, templateVersionId: string, + presetId: string | null = null, ): Promise { - const presets = await API.getTemplateVersionPresets(templateVersionId); - const defaultPreset = presets?.find((p) => p.Default); + // If no preset is selected, get the default preset + let preset_id: string | undefined = presetId || undefined; + if (!preset_id) { + const presets = await API.getTemplateVersionPresets(templateVersionId); + const defaultPreset = presets?.find((p) => p.Default); + preset_id = defaultPreset?.ID; + } + const workspace = await API.createWorkspace(userId, { name: `task-${generateWorkspaceName()}`, template_version_id: templateVersionId, - template_version_preset_id: defaultPreset?.ID, + template_version_preset_id: preset_id || undefined, rich_parameter_values: [ { name: AI_PROMPT_PARAMETER_NAME, value: prompt }, ], From 8cafbc9825206e1b407cff232a9d13704f56649e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 11:11:43 +0100 Subject: [PATCH 02/17] sort presets with default at top --- site/src/pages/TasksPage/TasksPage.tsx | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 93be42e8e4879..bdddac2c01b45 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -364,13 +364,21 @@ const TaskForm: FC = ({ templates, onSuccess }) => { - {presets.map((preset) => ( - - - {preset.Name} {preset.Default && "(Default)"} - - - ))} + {presets + .sort((a, b) => { + // Default preset should come first + if (a.Default && !b.Default) return -1; + if (!a.Default && b.Default) return 1; + // Otherwise, sort alphabetically by name + return a.Name.localeCompare(b.Name); + }) + .map((preset) => ( + + + {preset.Name} {preset.Default && "(Default)"} + + + ))} )} From 071c8968898632ce6353afb078fd4bf11ff4339f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 11:13:53 +0100 Subject: [PATCH 03/17] add labels for template and preset selectors --- site/src/pages/TasksPage/TasksPage.tsx | 106 +++++++++++++++---------- 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index bdddac2c01b45..6e652ab16259e 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -331,56 +331,80 @@ const TaskForm: FC = ({ templates, onSuccess }) => { text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm`} />
-
- - - {presets && presets.length > 0 && ( +
+
+ +
+ + {presets && presets.length > 0 && ( +
+ + +
)}
From 9938038c7abe05789e682ad51a6258bdb0f452ca Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 11:18:07 +0100 Subject: [PATCH 04/17] increase template and preset select width --- site/src/pages/TasksPage/TasksPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 6e652ab16259e..90955451ee5d5 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -347,7 +347,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { > @@ -382,7 +382,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { > From ec3d72d9f7386e03b7e17f29bda8853872cd0e51 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 11:29:00 +0100 Subject: [PATCH 05/17] override ai prompt with preset if defined --- site/src/pages/TasksPage/TasksPage.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 90955451ee5d5..5cfc77868f6e8 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -236,6 +236,13 @@ const TaskForm: FC = ({ templates, onSuccess }) => { const selectedTemplate = templates.find( (t) => t.id === selectedTemplateId, ) as Template; + + // Extract AI prompt from selected preset + const selectedPreset = presets?.find((p) => p.ID === selectedPresetId); + const presetAIPrompt = selectedPreset?.Parameters.find( + (param) => param.Name === "ai_prompt", + )?.Value; + const isPromptReadOnly = !!presetAIPrompt; const { externalAuth, externalAuthError, @@ -291,8 +298,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { const form = e.currentTarget; const formData = new FormData(form); - const prompt = formData.get("prompt") as string; - const templateID = formData.get("templateID") as string; + const prompt = presetAIPrompt || (formData.get("prompt") as string); try { await createTaskMutation.mutateAsync({ @@ -326,9 +332,13 @@ const TaskForm: FC = ({ templates, onSuccess }) => { required id="prompt" name="prompt" + value={presetAIPrompt || undefined} + readOnly={isPromptReadOnly} placeholder={textareaPlaceholder} className={`border-0 resize-none w-full h-full bg-transparent rounded-lg outline-none flex min-h-[60px] - text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm`} + text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm ${ + isPromptReadOnly ? "opacity-60 cursor-not-allowed" : "" + }`} />
From 31901f94dab76c0a8664a928b5cdb027e03b7691 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 11:42:45 +0100 Subject: [PATCH 06/17] hide presets selector if no presets exist for template --- site/src/pages/TasksPage/TasksPage.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 5cfc77868f6e8..450a324d2b145 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -260,14 +260,19 @@ const TaskForm: FC = ({ templates, onSuccess }) => { // Handle preset data changes useEffect(() => { - if (presetsData) { + if (presetsData !== undefined) { setPresets(presetsData); - // Set default preset if available - const defaultPreset = presetsData.find((p: Preset) => p.Default); - if (defaultPreset) { - setSelectedPresetId(defaultPreset.ID); - } else { + // Reset selected preset when changing templates or when no presets available + if (presetsData === null || presetsData.length === 0) { setSelectedPresetId(null); + } else { + // Set default preset if available + const defaultPreset = presetsData.find((p: Preset) => p.Default); + if (defaultPreset) { + setSelectedPresetId(defaultPreset.ID); + } else { + setSelectedPresetId(null); + } } } }, [presetsData]); From c4dbd2f2547c37604599ab6658c8dae0278a999d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 12:19:03 +0100 Subject: [PATCH 07/17] fix ai prompt parameter name --- site/src/pages/TasksPage/TasksPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 450a324d2b145..05a64e2c38946 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -240,7 +240,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { // Extract AI prompt from selected preset const selectedPreset = presets?.find((p) => p.ID === selectedPresetId); const presetAIPrompt = selectedPreset?.Parameters.find( - (param) => param.Name === "ai_prompt", + (param) => param.Name === AI_PROMPT_PARAMETER_NAME, )?.Value; const isPromptReadOnly = !!presetAIPrompt; const { From 72db7b69baad476492a9f00526721d558cb518f8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 12:19:17 +0100 Subject: [PATCH 08/17] update storybook --- .../src/pages/TasksPage/TasksPage.stories.tsx | 125 ++++++++++-------- site/src/testHelpers/entities.ts | 98 ++++++++++++++ 2 files changed, 167 insertions(+), 56 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.stories.tsx b/site/src/pages/TasksPage/TasksPage.stories.tsx index 1b1770f586768..85052484d31f0 100644 --- a/site/src/pages/TasksPage/TasksPage.stories.tsx +++ b/site/src/pages/TasksPage/TasksPage.stories.tsx @@ -4,12 +4,14 @@ import { API } from "api/api"; import { MockUsers } from "pages/UsersPage/storybookData/users"; import { reactRouterParameters } from "storybook-addon-remix-react-router"; import { + MockAIPromptPresets, + MockNewTaskData, + MockPresets, + MockTasks, MockTemplate, MockTemplateVersionExternalAuthGithub, MockTemplateVersionExternalAuthGithubAuthenticated, MockUserOwner, - MockWorkspace, - MockWorkspaceAppStatus, mockApiError, } from "testHelpers/entities"; import { @@ -31,6 +33,7 @@ const meta: Meta = { }, beforeEach: () => { spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([]); + spyOn(API, "getTemplateVersionPresets").mockResolvedValue(null); spyOn(API, "getUsers").mockResolvedValue({ users: MockUsers, count: MockUsers.length, @@ -53,7 +56,7 @@ type Story = StoryObj; export const LoadingAITemplates: Story = { beforeEach: () => { spyOn(data, "fetchAITemplates").mockImplementation( - () => new Promise((res) => 1000 * 60 * 60), + () => new Promise(() => 1000 * 60 * 60), ); }, }; @@ -79,7 +82,7 @@ export const LoadingTasks: Story = { beforeEach: () => { spyOn(data, "fetchAITemplates").mockResolvedValue([MockTemplate]); spyOn(data, "fetchTasks").mockImplementation( - () => new Promise((res) => 1000 * 60 * 60), + () => new Promise(() => 1000 * 60 * 60), ); }, play: async ({ canvasElement, step }) => { @@ -119,15 +122,57 @@ export const LoadedTasks: Story = { }, }; -const newTaskData = { - prompt: "Create a new task", - workspace: { - ...MockWorkspace, - id: "workspace-4", - latest_app_status: { - ...MockWorkspaceAppStatus, - message: "Task created successfully!", - }, +export const LoadedTasksWithPresets: Story = { + decorators: [withProxyProvider()], + beforeEach: () => { + const mockTemplateWithPresets = { + ...MockTemplate, + id: "test-template-2", + name: "template-with-presets", + display_name: "Template with Presets", + }; + + spyOn(data, "fetchAITemplates").mockResolvedValue([ + MockTemplate, + mockTemplateWithPresets, + ]); + spyOn(data, "fetchTasks").mockResolvedValue(MockTasks); + spyOn(API, "getTemplateVersionPresets").mockImplementation( + async (versionId) => { + // Return presets only for the second template + if (versionId === mockTemplateWithPresets.active_version_id) { + return MockPresets; + } + return null; + }, + ); + }, +}; + +export const LoadedTasksWithAIPromptPresets: Story = { + decorators: [withProxyProvider()], + beforeEach: () => { + const mockTemplateWithPresets = { + ...MockTemplate, + id: "test-template-2", + name: "template-with-presets", + display_name: "Template with AI Prompt Presets", + }; + + spyOn(data, "fetchAITemplates").mockResolvedValue([ + MockTemplate, + mockTemplateWithPresets, + ]); + spyOn(data, "fetchTasks").mockResolvedValue(MockTasks); + spyOn(API, "getTemplateVersionPresets").mockImplementation( + async (versionId) => { + // Return presets only for the second template + if (versionId === mockTemplateWithPresets.active_version_id) { + return MockAIPromptPresets; + } + return null; + }, + ); }, }; @@ -154,15 +199,15 @@ export const CreateTaskSuccessfully: Story = { spyOn(data, "fetchAITemplates").mockResolvedValue([MockTemplate]); spyOn(data, "fetchTasks") .mockResolvedValueOnce(MockTasks) - .mockResolvedValue([newTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(newTaskData); + .mockResolvedValue([MockNewTaskData, ...MockTasks]); + spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step("Run task", async () => { const prompt = await canvas.findByLabelText(/prompt/i); - await userEvent.type(prompt, newTaskData.prompt); + await userEvent.type(prompt, MockNewTaskData.prompt); const submitButton = canvas.getByRole("button", { name: /run task/i }); await waitFor(() => expect(submitButton).toBeEnabled()); await userEvent.click(submitButton); @@ -208,8 +253,8 @@ export const WithAuthenticatedExternalAuth: Story = { beforeEach: () => { spyOn(data, "fetchTasks") .mockResolvedValueOnce(MockTasks) - .mockResolvedValue([newTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(newTaskData); + .mockResolvedValue([MockNewTaskData, ...MockTasks]); + spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([ MockTemplateVersionExternalAuthGithubAuthenticated, ]); @@ -235,8 +280,8 @@ export const MissingExternalAuth: Story = { beforeEach: () => { spyOn(data, "fetchTasks") .mockResolvedValueOnce(MockTasks) - .mockResolvedValue([newTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(newTaskData); + .mockResolvedValue([MockNewTaskData, ...MockTasks]); + spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([ MockTemplateVersionExternalAuthGithub, ]); @@ -246,7 +291,7 @@ export const MissingExternalAuth: Story = { await step("Submit is disabled", async () => { const prompt = await canvas.findByLabelText(/prompt/i); - await userEvent.type(prompt, newTaskData.prompt); + await userEvent.type(prompt, MockNewTaskData.prompt); const submitButton = canvas.getByRole("button", { name: /run task/i }); expect(submitButton).toBeDisabled(); }); @@ -262,8 +307,8 @@ export const ExternalAuthError: Story = { beforeEach: () => { spyOn(data, "fetchTasks") .mockResolvedValueOnce(MockTasks) - .mockResolvedValue([newTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(newTaskData); + .mockResolvedValue([MockNewTaskData, ...MockTasks]); + spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); spyOn(API, "getTemplateVersionExternalAuth").mockRejectedValue( mockApiError({ message: "Failed to load external auth", @@ -275,7 +320,7 @@ export const ExternalAuthError: Story = { await step("Submit is disabled", async () => { const prompt = await canvas.findByLabelText(/prompt/i); - await userEvent.type(prompt, newTaskData.prompt); + await userEvent.type(prompt, MockNewTaskData.prompt); const submitButton = canvas.getByRole("button", { name: /run task/i }); expect(submitButton).toBeDisabled(); }); @@ -308,35 +353,3 @@ export const NonAdmin: Story = { }); }, }; - -const MockTasks = [ - { - workspace: { - ...MockWorkspace, - latest_app_status: MockWorkspaceAppStatus, - }, - prompt: "Create competitors page", - }, - { - workspace: { - ...MockWorkspace, - id: "workspace-2", - latest_app_status: { - ...MockWorkspaceAppStatus, - message: "Avatar size fixed!", - }, - }, - prompt: "Fix user avatar size", - }, - { - workspace: { - ...MockWorkspace, - id: "workspace-3", - latest_app_status: { - ...MockWorkspaceAppStatus, - message: "Accessibility issues fixed!", - }, - }, - prompt: "Fix accessibility issues", - }, -]; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 045d6ad06ddeb..fe7202814d846 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -4571,3 +4571,101 @@ export function createTimestamp(minuteOffset: number, secondOffset: number) { baseDate.setSeconds(baseDate.getSeconds() + secondOffset); return baseDate.toISOString(); } + +// Mock Presets for AI Tasks +export const MockPresets: TypesGen.Preset[] = [ + { + ID: "preset-1", + Name: "Development", + Parameters: [ + { Name: "cpu", Value: "4" }, + { Name: "memory", Value: "8GB" }, + ], + Default: true, + }, + { + ID: "preset-2", + Name: "Testing", + Parameters: [ + { Name: "cpu", Value: "2" }, + { Name: "memory", Value: "4GB" }, + ], + Default: false, + }, + { + ID: "preset-3", + Name: "Production", + Parameters: [ + { Name: "cpu", Value: "8" }, + { Name: "memory", Value: "16GB" }, + ], + Default: false, + }, +]; + +export const MockAIPromptPresets: TypesGen.Preset[] = [ + { + ID: "ai-preset-1", + Name: "Code Review", + Parameters: [ + { Name: "AI Prompt", Value: "Review the code for best practices" }, + { Name: "cpu", Value: "4" }, + { Name: "memory", Value: "8GB" }, + ], + Default: true, + }, + { + ID: "ai-preset-2", + Name: "Custom Prompt", + Parameters: [ + { Name: "cpu", Value: "4" }, + { Name: "memory", Value: "8GB" }, + ], + Default: false, + }, +]; + +// Mock Tasks for AI Tasks page +export const MockTasks = [ + { + workspace: { + ...MockWorkspace, + latest_app_status: MockWorkspaceAppStatus, + }, + prompt: "Create competitors page", + }, + { + workspace: { + ...MockWorkspace, + id: "workspace-2", + latest_app_status: { + ...MockWorkspaceAppStatus, + message: "Avatar size fixed!", + }, + }, + prompt: "Fix user avatar size", + }, + { + workspace: { + ...MockWorkspace, + id: "workspace-3", + latest_app_status: { + ...MockWorkspaceAppStatus, + message: "Accessibility issues fixed!", + }, + }, + prompt: "Fix accessibility issues", + }, +]; + +export const MockNewTaskData = { + prompt: "Create a new task", + workspace: { + ...MockWorkspace, + id: "workspace-4", + latest_app_status: { + ...MockWorkspaceAppStatus, + message: "Task created successfully!", + }, + }, +}; From 4f2d820a1bf8c5a80b260219dad247fad8d065cf Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 12:55:45 +0100 Subject: [PATCH 09/17] address copilot comments --- site/src/pages/TasksPage/TasksPage.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 05a64e2c38946..5efc6584cf3ae 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -715,11 +715,13 @@ export const data = { presetId: string | null = null, ): Promise { // If no preset is selected, get the default preset - let preset_id: string | undefined = presetId || undefined; + let preset_id = presetId; if (!preset_id) { const presets = await API.getTemplateVersionPresets(templateVersionId); const defaultPreset = presets?.find((p) => p.Default); - preset_id = defaultPreset?.ID; + if (defaultPreset) { + preset_id = defaultPreset.ID; + } } const workspace = await API.createWorkspace(userId, { From 7602ef6d626650a37b828fc3d49e1d7ed408b3af Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 14:23:53 +0100 Subject: [PATCH 10/17] address code review comments --- site/src/pages/TasksPage/TasksPage.tsx | 55 ++++++++++++-------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 5efc6584cf3ae..6d7691897e66b 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -260,21 +260,14 @@ const TaskForm: FC = ({ templates, onSuccess }) => { // Handle preset data changes useEffect(() => { - if (presetsData !== undefined) { - setPresets(presetsData); - // Reset selected preset when changing templates or when no presets available - if (presetsData === null || presetsData.length === 0) { - setSelectedPresetId(null); - } else { - // Set default preset if available - const defaultPreset = presetsData.find((p: Preset) => p.Default); - if (defaultPreset) { - setSelectedPresetId(defaultPreset.ID); - } else { - setSelectedPresetId(null); - } - } + if (!presetsData) { + setPresets(null); + return; } + setPresets(presetsData); + const defaultPreset = presetsData.find((p: Preset) => p.Default); + const defaultPresetID = defaultPreset?.ID || null; + setSelectedPresetId(defaultPresetID); }, [presetsData]); const missedExternalAuth = externalAuth?.filter( (auth) => !auth.optional && !auth.authenticated, @@ -402,21 +395,13 @@ const TaskForm: FC = ({ templates, onSuccess }) => { - {presets - .sort((a, b) => { - // Default preset should come first - if (a.Default && !b.Default) return -1; - if (!a.Default && b.Default) return 1; - // Otherwise, sort alphabetically by name - return a.Name.localeCompare(b.Name); - }) - .map((preset) => ( - - - {preset.Name} {preset.Default && "(Default)"} - - - ))} + {sortedPresets(presets).map((preset) => ( + + + {preset.Name} {preset.Default && "(Default)"} + + + ))}
@@ -740,4 +725,16 @@ export const data = { }, }; +// sortedPresets sorts presets with the default preset first, +// followed by the rest sorted alphabetically by name ascending. +const sortedPresets = (presets: Preset[]): Preset[] => { + return presets.sort((a, b) => { + // Default preset should come first + if (a.Default && !b.Default) return -1; + if (!a.Default && b.Default) return 1; + // Otherwise, sort alphabetically by name + return a.Name.localeCompare(b.Name); + }); +}; + export default TasksPage; From 80b96de8b2dd05e5accbf9eed979bec017cbf136 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 16:25:53 +0100 Subject: [PATCH 11/17] fix crash if presets is null --- .../src/pages/TasksPage/TasksPage.stories.tsx | 20 +++++++++++++++++++ site/src/pages/TasksPage/TasksPage.tsx | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TasksPage/TasksPage.stories.tsx b/site/src/pages/TasksPage/TasksPage.stories.tsx index 85052484d31f0..0bdc9d27a7eed 100644 --- a/site/src/pages/TasksPage/TasksPage.stories.tsx +++ b/site/src/pages/TasksPage/TasksPage.stories.tsx @@ -176,6 +176,26 @@ export const LoadedTasksWithAIPromptPresets: Story = { }, }; +export const LoadedTasksEdgeCases: Story = { + decorators: [withProxyProvider()], + beforeEach: () => { + spyOn(data, "fetchAITemplates").mockResolvedValue([MockTemplate]); + spyOn(data, "fetchTasks").mockResolvedValue(MockTasks); + + // Test various edge cases for presets + spyOn(API, "getTemplateVersionPresets").mockImplementation(async () => { + return [ + { + ID: "malformed", + Name: "Malformed Preset", + Default: true, + }, + // biome-ignore lint/suspicious/noExplicitAny: Testing malformed data edge cases + ] as any; + }); + }, +}; + export const CreateTaskSuccessfully: Story = { decorators: [withProxyProvider()], parameters: { diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 6d7691897e66b..c327bedee818b 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -239,7 +239,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { // Extract AI prompt from selected preset const selectedPreset = presets?.find((p) => p.ID === selectedPresetId); - const presetAIPrompt = selectedPreset?.Parameters.find( + const presetAIPrompt = selectedPreset?.Parameters?.find( (param) => param.Name === AI_PROMPT_PARAMETER_NAME, )?.Value; const isPromptReadOnly = !!presetAIPrompt; From e248c147a04500ed7f20fb7f650d1fad568eaf65 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 16:58:38 +0100 Subject: [PATCH 12/17] adjust label styling --- site/src/pages/TasksPage/TasksPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index c327bedee818b..ba24f04a7e4df 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -343,7 +343,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => {
@@ -377,7 +377,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => {
From bf6f11303f39ea290e81db85341aa5e46617bb34 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 17:58:43 +0100 Subject: [PATCH 13/17] correctly handle selecting default preset on presetData change --- site/src/pages/TasksPage/TasksPage.tsx | 88 +++++++++++++++++--------- 1 file changed, 57 insertions(+), 31 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index ba24f04a7e4df..385e2e9a6aa52 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -231,18 +231,11 @@ const TaskForm: FC = ({ templates, onSuccess }) => { const [selectedTemplateId, setSelectedTemplateId] = useState( templates[0].id, ); - const [presets, setPresets] = useState(null); const [selectedPresetId, setSelectedPresetId] = useState(null); const selectedTemplate = templates.find( (t) => t.id === selectedTemplateId, ) as Template; - // Extract AI prompt from selected preset - const selectedPreset = presets?.find((p) => p.ID === selectedPresetId); - const presetAIPrompt = selectedPreset?.Parameters?.find( - (param) => param.Name === AI_PROMPT_PARAMETER_NAME, - )?.Value; - const isPromptReadOnly = !!presetAIPrompt; const { externalAuth, externalAuthError, @@ -251,24 +244,41 @@ const TaskForm: FC = ({ templates, onSuccess }) => { } = useExternalAuth(selectedTemplate.active_version_id); // Fetch presets when template changes - const { data: presetsData } = useQuery({ + const { data: presetsData, isLoading: isLoadingPresets } = useQuery< + Preset[] | null, + Error + >({ queryKey: ["template-version-presets", selectedTemplate.active_version_id], queryFn: () => API.getTemplateVersionPresets(selectedTemplate.active_version_id), ...disabledRefetchOptions, }); - // Handle preset data changes + // Handle preset selection when data changes useEffect(() => { - if (!presetsData) { - setPresets(null); + if (presetsData === undefined) { + // Still loading + return; + } + + if (!presetsData || presetsData.length === 0) { + setSelectedPresetId(null); return; } - setPresets(presetsData); + + // Always select the default preset when new data arrives const defaultPreset = presetsData.find((p: Preset) => p.Default); const defaultPresetID = defaultPreset?.ID || null; setSelectedPresetId(defaultPresetID); }, [presetsData]); + + // Extract AI prompt from selected preset + const selectedPreset = presetsData?.find((p) => p.ID === selectedPresetId); + const presetAIPrompt = selectedPreset?.Parameters?.find( + (param) => param.Name === AI_PROMPT_PARAMETER_NAME, + )?.Value; + const isPromptReadOnly = !!presetAIPrompt; + const missedExternalAuth = externalAuth?.filter( (auth) => !auth.optional && !auth.authenticated, ); @@ -373,39 +383,55 @@ const TaskForm: FC = ({ templates, onSuccess }) => {
- {presets && presets.length > 0 && ( -
- +
+ + {isLoadingPresets ? ( + + ) : ( -
- )} + )} +
From b3916e29c06902e922716eb600701d27799c34b8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 18:20:23 +0100 Subject: [PATCH 14/17] DesiredPrebuildInstances --- site/src/testHelpers/entities.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index fe7202814d846..14fbb2d2913af 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -4582,6 +4582,7 @@ export const MockPresets: TypesGen.Preset[] = [ { Name: "memory", Value: "8GB" }, ], Default: true, + DesiredPrebuildInstances: 0, }, { ID: "preset-2", @@ -4591,6 +4592,7 @@ export const MockPresets: TypesGen.Preset[] = [ { Name: "memory", Value: "4GB" }, ], Default: false, + DesiredPrebuildInstances: 0, }, { ID: "preset-3", @@ -4600,6 +4602,7 @@ export const MockPresets: TypesGen.Preset[] = [ { Name: "memory", Value: "16GB" }, ], Default: false, + DesiredPrebuildInstances: 0, }, ]; @@ -4613,6 +4616,7 @@ export const MockAIPromptPresets: TypesGen.Preset[] = [ { Name: "memory", Value: "8GB" }, ], Default: true, + DesiredPrebuildInstances: 0, }, { ID: "ai-preset-2", @@ -4622,6 +4626,7 @@ export const MockAIPromptPresets: TypesGen.Preset[] = [ { Name: "memory", Value: "8GB" }, ], Default: false, + DesiredPrebuildInstances: 0, }, ]; From 0fd55e295bc142270f9f77c0d4688f9b46923954 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 18:38:16 +0100 Subject: [PATCH 15/17] add label to indicate if prompt is defined by preset --- site/src/pages/TasksPage/TasksPage.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 385e2e9a6aa52..b0c4afbb37743 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -333,9 +333,14 @@ const TaskForm: FC = ({ templates, onSuccess }) => { className="border border-border border-solid rounded-lg p-4" disabled={createTaskMutation.isPending} > - + {isPromptReadOnly && ( + + )} Date: Thu, 24 Jul 2025 19:46:26 +0100 Subject: [PATCH 16/17] use preexisting query --- site/src/pages/TasksPage/TasksPage.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index b0c4afbb37743..28f2624cd9f90 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -40,6 +40,7 @@ import { TableRowSkeleton, } from "components/TableLoader/TableLoader"; +import { templateVersionPresets } from "api/queries/templates"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { @@ -247,12 +248,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { const { data: presetsData, isLoading: isLoadingPresets } = useQuery< Preset[] | null, Error - >({ - queryKey: ["template-version-presets", selectedTemplate.active_version_id], - queryFn: () => - API.getTemplateVersionPresets(selectedTemplate.active_version_id), - ...disabledRefetchOptions, - }); + >(templateVersionPresets(selectedTemplate.active_version_id)); // Handle preset selection when data changes useEffect(() => { From 26801113c929adf4d03e61410beae2a1583c747e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 19:48:45 +0100 Subject: [PATCH 17/17] keep form label around --- site/src/pages/TasksPage/TasksPage.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 28f2624cd9f90..4866dbfb49222 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -329,14 +329,16 @@ const TaskForm: FC = ({ templates, onSuccess }) => { className="border border-border border-solid rounded-lg p-4" disabled={createTaskMutation.isPending} > - {isPromptReadOnly && ( - - )} +