diff --git a/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.spec.ts b/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.spec.ts
index b57b940f1c9..5bd83b5941b 100644
--- a/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.spec.ts
+++ b/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.spec.ts
@@ -1,6 +1,6 @@
import { OrganizationEventApi, OrganizationEventTargetType } from 'qovery-typescript-axios'
import { type SelectedItem } from '@qovery/shared/ui'
-import { computeMenusToDisplay } from './target-type-selection-utils'
+import { computeMenusToDisplay, getTargetTypeLabel } from './target-type-selection-utils'
const mockGetOrganizationEventTargets = jest.spyOn(OrganizationEventApi.prototype, 'getOrganizationEventTargets')
@@ -8,7 +8,7 @@ const targetTypeItem: SelectedItem = {
filterKey: 'target_type',
item: {
value: OrganizationEventTargetType.AGENTIC_WORKFLOW,
- name: 'Agentic workflow',
+ name: 'Agent task',
},
}
@@ -85,3 +85,13 @@ describe('computeMenusToDisplay', () => {
)
})
})
+
+describe('getTargetTypeLabel', () => {
+ it('uses the Agent task product name for the agentic workflow API type', () => {
+ expect(getTargetTypeLabel(OrganizationEventTargetType.AGENTIC_WORKFLOW)).toBe('Agent task')
+ })
+
+ it('formats other API target types', () => {
+ expect(getTargetTypeLabel(OrganizationEventTargetType.CONTAINER_REGISTRY)).toBe('Container registry')
+ })
+})
diff --git a/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.tsx b/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.tsx
index e56246307e6..807c1dc989f 100644
--- a/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.tsx
+++ b/libs/domains/audit-logs/feature/src/lib/utils/target-type-selection-utils.tsx
@@ -6,6 +6,7 @@ import {
type NavigationLevel,
type SelectedItem,
} from '@qovery/shared/ui'
+import { upperCaseFirstLetter } from '@qovery/shared/util-js'
const SERVICE_TARGET_TYPES: ReadonlySet = new Set([
OrganizationEventTargetType.AGENTIC_WORKFLOW,
@@ -17,6 +18,12 @@ const SERVICE_TARGET_TYPES: ReadonlySet = new Set([
OrganizationEventTargetType.TERRAFORM,
])
+export function getTargetTypeLabel(targetType: string) {
+ return targetType === OrganizationEventTargetType.AGENTIC_WORKFLOW
+ ? 'Agent task'
+ : upperCaseFirstLetter(targetType).replace(/_/g, ' ')
+}
+
function getTargetTypeSelected(selectedItems: SelectedItem[]): OrganizationEventTargetType | undefined {
const selectedTargetType = selectedItems.find((selectedItem) => {
return selectedItem.filterKey === 'target_type'
diff --git a/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx b/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx
index ff343493176..0be8a4fb4da 100644
--- a/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx
+++ b/libs/domains/environments/feature/src/lib/environments-table/environment-section/environment-section.tsx
@@ -1,6 +1,6 @@
import { Link, useNavigate, useParams } from '@tanstack/react-router'
import { EnvironmentModeEnum, type EnvironmentOverviewResponse, StateEnum } from 'qovery-typescript-axios'
-import { type KeyboardEvent, type MouseEvent } from 'react'
+import { type KeyboardEvent, type MouseEvent, useMemo } from 'react'
import { match } from 'ts-pattern'
import { ClusterAvatar } from '@qovery/domains/clusters/feature'
import { Button, Checkbox, DeploymentAction, Heading, Icon, Section, TablePrimitives, Tooltip } from '@qovery/shared/ui'
@@ -187,6 +187,11 @@ function EnvRow({
)
}
+function lastOperationTimestamp(overview: EnvironmentOverviewResponse) {
+ const lastDeploymentDate = overview.deployment_status?.last_deployment_date
+ return lastDeploymentDate ? new Date(lastDeploymentDate).getTime() : 0
+}
+
export function EnvironmentSection({
type,
items,
@@ -209,6 +214,16 @@ export function EnvironmentSection({
.with('PREVIEW', () => 'Ephemeral')
.exhaustive()
+ const sortedItems = useMemo(() => {
+ if (type !== EnvironmentModeEnum.PREVIEW) {
+ return items
+ }
+
+ return [...items].sort(
+ (environmentA, environmentB) => lastOperationTimestamp(environmentB) - lastOperationTimestamp(environmentA)
+ )
+ }, [items, type])
+
const EmptyState = () =>
match(type)
.with(EnvironmentModeEnum.PREVIEW, () => {
@@ -295,7 +310,7 @@ export function EnvironmentSection({
- {items.map((environmentOverview) => (
+ {sortedItems.map((environmentOverview) => (
({
EnvironmentsTableActionBar: () => ,
}))
-function environmentOverview(id: string, mode: EnvironmentModeEnum, name: string): EnvironmentOverviewResponse {
+function environmentOverview(
+ id: string,
+ mode: EnvironmentModeEnum,
+ name: string,
+ lastDeploymentDate?: string
+): EnvironmentOverviewResponse {
return {
id,
mode,
@@ -48,6 +53,7 @@ function environmentOverview(id: string, mode: EnvironmentModeEnum, name: string
service_count: 0,
managed_by: 'QOVERY',
},
+ ...(lastDeploymentDate ? { deployment_status: { last_deployment_date: lastDeploymentDate } } : {}),
} as EnvironmentOverviewResponse
}
@@ -94,6 +100,25 @@ describe('EnvironmentsTable', () => {
])
})
+ it('should sort ephemeral environments by last operation with newer environments first', () => {
+ mockUseProject.mockReturnValue({ data: { name: 'Project Alpha' } })
+ mockUseEnvironmentsOverview.mockReturnValue({
+ data: [
+ environmentOverview('env-1', EnvironmentModeEnum.PREVIEW, 'Bravo', '2024-03-01T00:00:00Z'),
+ environmentOverview('env-2', EnvironmentModeEnum.PREVIEW, 'Alpha', '2024-01-01T00:00:00Z'),
+ environmentOverview('env-3', EnvironmentModeEnum.PREVIEW, 'Charlie', '2024-02-01T00:00:00Z'),
+ ],
+ })
+
+ renderWithProviders()
+
+ expect(screen.getAllByRole('link', { name: /^(Alpha|Bravo|Charlie)$/ }).map((link) => link.textContent)).toEqual([
+ 'Bravo',
+ 'Charlie',
+ 'Alpha',
+ ])
+ })
+
it('should preserve checkbox focus when selecting an environment', async () => {
mockUseProject.mockReturnValue({ data: { name: 'Project Alpha' } })
mockUseEnvironmentsOverview.mockReturnValue({
diff --git a/libs/domains/onboarding/feature/src/lib/step-use-cases/step-use-cases.tsx b/libs/domains/onboarding/feature/src/lib/step-use-cases/step-use-cases.tsx
index 5d30f00d779..4c41d8f9914 100644
--- a/libs/domains/onboarding/feature/src/lib/step-use-cases/step-use-cases.tsx
+++ b/libs/domains/onboarding/feature/src/lib/step-use-cases/step-use-cases.tsx
@@ -9,21 +9,16 @@ const USE_CASES: Array<{ value: string; label: string; iconName: IconName }> = [
label: 'Build workflows where AI can take actions on my systems with full auditability',
iconName: 'microchip-ai',
},
- {
- value: 'rde',
- label: 'Enable my non-tech team to ship apps',
- iconName: 'users',
- },
- {
- value: 'spec-to-prod',
- label: 'Go from spec to production with AI coding agents',
- iconName: 'diagram-project',
- },
{
value: 'automate-deployments',
label: 'Automate deployments without manual steps',
iconName: 'rocket',
},
+ {
+ value: 'migrate-cloud-provider',
+ label: 'Migrate to another cloud provider from Heroku, Render or another PaaS',
+ iconName: 'right-left',
+ },
{
value: 'ephemeral-environments',
label: 'Create environments on demand (testing/dev/QA)',
@@ -40,12 +35,11 @@ interface UseCaseCardProps {
value: string
label: string
iconName: IconName
- colSpan: string
selected: boolean
onToggle: (value: string) => void
}
-function UseCaseCard({ value, label, iconName, colSpan, selected, onToggle }: UseCaseCardProps) {
+function UseCaseCard({ value, label, iconName, selected, onToggle }: UseCaseCardProps) {
return (
What are you looking to do?
Select all that apply.
-
+
{USE_CASES.map((useCase) => (
@@ -106,7 +98,7 @@ export function StepUseCases({ onSubmit, onBack }: StepUseCasesProps) {
Back
-
diff --git a/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx b/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx
index 74c09ee972e..17ac517f938 100644
--- a/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx
+++ b/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx
@@ -42,19 +42,19 @@ interface FormValues {
}
const PAGE_CONTENT: Record = {
- general: { title: 'General settings', description: 'Configure the workflow name, description, and availability.' },
+ general: { title: 'General settings', description: 'Configure the agent task name, description, and availability.' },
'ai-configuration': {
title: 'AI configuration',
- description: 'Configure the model and instructions used by this workflow.',
+ description: 'Configure the model and instructions used by this agent task.',
},
connections: {
title: 'Connections',
- description: 'Configure the Git repositories, MCP servers, and Dockerfile fragment available to the workflow.',
+ description: 'Configure the Git repositories, MCP servers, and Dockerfile fragment available to the agent task.',
},
- outputs: { title: 'Outputs', description: 'Configure the webhooks called when the workflow produces an output.' },
+ outputs: { title: 'Outputs', description: 'Configure the webhooks called when the agent task produces an output.' },
governance: {
title: 'Governance',
- description: 'Control the hosts and webhook source addresses allowed for this workflow.',
+ description: 'Control the hosts and webhook source addresses allowed for this agent task.',
},
}
@@ -241,7 +241,7 @@ export function AgenticWorkflowSettings({ page }: AgenticWorkflowSettingsProps)
name="name"
label="Name"
value={values.name}
- error={!values.name.trim() ? 'Please enter a workflow name.' : undefined}
+ error={!values.name.trim() ? 'Please enter an agent task name.' : undefined}
onChange={(event) => form.setValue('name', event.currentTarget.value, { shouldDirty: true })}
/>
form.setValue('enabled', value, { shouldDirty: true })}
/>
Resources
- Configure the compute resources allocated to the workflow.
+ Configure the compute resources allocated to the agent task.
{(['cpu', 'ram', 'gpu', 'storage'] as const).map((name) => (
@@ -303,7 +303,7 @@ export function AgenticWorkflowSettings({ page }: AgenticWorkflowSettingsProps)
Git repositories
- Select the repositories and branches the workflow can access.
+ Select the repositories and branches the agent task can access.
diff --git a/libs/domains/services/feature/src/lib/agentic-workflow-service-actions/agentic-workflow-service-actions.tsx b/libs/domains/services/feature/src/lib/agentic-workflow-service-actions/agentic-workflow-service-actions.tsx
index 1803b9af976..c5924b4a407 100644
--- a/libs/domains/services/feature/src/lib/agentic-workflow-service-actions/agentic-workflow-service-actions.tsx
+++ b/libs/domains/services/feature/src/lib/agentic-workflow-service-actions/agentic-workflow-service-actions.tsx
@@ -44,8 +44,7 @@ export function AgenticWorkflowServiceActions({
const deleteAgenticWorkflow = () => {
openModalConfirmation({
title: `Delete ${service.name}?`,
- description:
- 'This will permanently delete the agentic workflow and its associated data. This action cannot be undone.',
+ description: 'This will permanently delete the agent task and its associated data. This action cannot be undone.',
name: service.name,
action: async () => {
await deleteService({ serviceId: service.id, serviceType: service.serviceType })
diff --git a/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.spec.tsx b/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.spec.tsx
index 91c7c177020..7f402f43558 100644
--- a/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.spec.tsx
+++ b/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.spec.tsx
@@ -70,7 +70,7 @@ describe('AgenticWorkflowServiceList', () => {
})
renderWithProviders()
- expect(screen.getByRole('heading', { name: 'Agentic workflows' })).toBeInTheDocument()
+ expect(screen.getByRole('heading', { name: 'Agent tasks' })).toBeInTheDocument()
expect(screen.getByText('Review pull requests')).toBeInTheDocument()
expect(screen.getByText('Triage incidents')).toBeInTheDocument()
expect(screen.getByText('Enabled')).toBeInTheDocument()
diff --git a/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.tsx b/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.tsx
index 9c96aec05f6..05fd5ca906c 100644
--- a/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.tsx
+++ b/libs/domains/services/feature/src/lib/agentic-workflow-service-list/agentic-workflow-service-list.tsx
@@ -84,9 +84,9 @@ export function AgenticWorkflowServiceList({ environment }: AgenticWorkflowServi
- Agentic workflows
+ Agent tasks
-
AI workflows triggered through webhooks and connected services.
- Configure the inputs, tools, model, governance, and outputs used by your workflow.
+ Configure the inputs, tools, model, governance, and outputs used by your agent task.
@@ -338,7 +338,7 @@ export function AgenticWorkflowConfiguration() {
label="Name"
value={values.name}
autoFocus
- error={showNameError ? 'Please enter a workflow name.' : undefined}
+ error={showNameError ? 'Please enter an agent task name.' : undefined}
onChange={(event) => form.setValue('name', event.currentTarget.value, { shouldDirty: true })}
/>
form.setValue('workflowEnabled', value, { shouldDirty: true })}
/>
@@ -708,7 +708,7 @@ export function AgenticWorkflowConfiguration() {
value={values.agentPrompt}
textareaClassName="min-h-40"
variableKeys={variableValues.map((variable) => variable.variable ?? '').filter(Boolean)}
- hint="Describe the workflow behavior. Example: review incoming webhook payloads, open a pull request when needed, then notify the team."
+ hint="Describe the agent task behavior. Example: review incoming webhook payloads, open a pull request when needed, then notify the team."
onChange={(value) => form.setValue('agentPrompt', value, { shouldDirty: true })}
/>
diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx
index 53234c61ce1..efa273ad679 100644
--- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx
+++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/ai-model-cards.tsx
@@ -22,7 +22,7 @@ export function AIModelCards() {
Anthropic
- Claude models used by the workflow agent.
+ Claude models used by the agent task.
- Ready to create your agentic workflow
+ Ready to create your agent task
- Review the workflow configuration before creating it. Webhook details will be generated after creation.
+ Review the agent task configuration before creating it. Webhook details will be generated after creation.
diff --git a/libs/domains/services/feature/src/lib/service-new/service-new.spec.tsx b/libs/domains/services/feature/src/lib/service-new/service-new.spec.tsx
index ace0ba99c94..892377aac16 100644
--- a/libs/domains/services/feature/src/lib/service-new/service-new.spec.tsx
+++ b/libs/domains/services/feature/src/lib/service-new/service-new.spec.tsx
@@ -131,7 +131,7 @@ describe('ServiceNew', () => {
expect(screen.getByText('Cron Job')).toBeInTheDocument()
expect(screen.getByText('Helm')).toBeInTheDocument()
expect(screen.getAllByText('Terraform').length).toBeGreaterThanOrEqual(1)
- expect(screen.queryByText('Agentic workflow')).not.toBeInTheDocument()
+ expect(screen.queryByText('Agent task')).not.toBeInTheDocument()
})
it('should render agentic workflow entry when feature flag is enabled', () => {
@@ -141,8 +141,8 @@ describe('ServiceNew', () => {
)
- expect(screen.getByText('Agentic workflow')).toBeInTheDocument()
- expect(screen.getByRole('link', { name: /Agentic workflow/i })).toHaveAttribute(
+ expect(screen.getByText('Agent task')).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /Agent task/i })).toHaveAttribute(
'href',
'/organization/org-1/project/project-1/environment/env-1/service/create/agentic-workflow'
)
diff --git a/libs/domains/services/feature/src/lib/service-new/service-new.tsx b/libs/domains/services/feature/src/lib/service-new/service-new.tsx
index f46f97ce0ae..0b683e1ed77 100644
--- a/libs/domains/services/feature/src/lib/service-new/service-new.tsx
+++ b/libs/domains/services/feature/src/lib/service-new/service-new.tsx
@@ -165,8 +165,8 @@ export function ServiceNew({
...(isAgenticWorkflowEnabled
? [
{
- title: 'Agentic workflow',
- description: 'Run an AI workflow with webhooks, MCPs, governance, and configured outputs.',
+ title: 'Agent task',
+ description: 'Delegate a one-time task to an AI agent with access to repositories, MCPs, and webhooks.',
icon: ,
link: getServicesPath(organizationId, projectId, environmentId, '/service/create/agentic-workflow'),
cloud_provider: cloudProvider,