-
-
- {service.name}
-
+ {services.map((service) => (
+
handleNavigateToService(service.id)}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') handleNavigateToService(service.id)
+ }}
+ >
+
+
+
+
-
- {service.model?.type ?? '-'}
+
+
+
+
+
-
- {service.enabled ? 'Enabled' : 'Disabled'}
-
+
+
+
))}
diff --git a/libs/domains/services/feature/src/lib/hooks/use-agentic-workflow-services/use-agentic-workflow-services.ts b/libs/domains/services/feature/src/lib/hooks/use-agentic-workflow-services/use-agentic-workflow-services.ts
deleted file mode 100644
index f712ad971e8..00000000000
--- a/libs/domains/services/feature/src/lib/hooks/use-agentic-workflow-services/use-agentic-workflow-services.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { useQuery } from '@tanstack/react-query'
-import { queries } from '@qovery/state/util-queries'
-
-export interface UseAgenticWorkflowServicesProps {
- environmentId: string
- suspense?: boolean
-}
-
-export function useAgenticWorkflowServices({ environmentId, suspense = false }: UseAgenticWorkflowServicesProps) {
- return useQuery({
- ...queries.services.listAgenticWorkflows(environmentId),
- suspense,
- })
-}
-
-export default useAgenticWorkflowServices
diff --git a/libs/domains/services/feature/src/lib/hooks/use-running-status/use-running-status.ts b/libs/domains/services/feature/src/lib/hooks/use-running-status/use-running-status.ts
index d31c461f263..3bb5d34be9a 100644
--- a/libs/domains/services/feature/src/lib/hooks/use-running-status/use-running-status.ts
+++ b/libs/domains/services/feature/src/lib/hooks/use-running-status/use-running-status.ts
@@ -4,14 +4,14 @@ import { queries } from '@qovery/state/util-queries'
export interface UseRunningStatusProps {
environmentId?: string
serviceId?: string
+ enabled?: boolean
suspense?: boolean
}
-export function useRunningStatus({ environmentId, serviceId, suspense }: UseRunningStatusProps) {
+export function useRunningStatus({ environmentId, serviceId, enabled = true, suspense }: UseRunningStatusProps) {
return useQuery({
- // eslint-disable-next-line @typescript-eslint/no-extra-non-null-assertion
- ...queries.services.runningStatus(environmentId!!, serviceId!!),
- enabled: Boolean(environmentId) && Boolean(serviceId),
+ ...queries.services.runningStatus(environmentId ?? '', serviceId ?? ''),
+ enabled: enabled && Boolean(environmentId) && Boolean(serviceId),
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
diff --git a/libs/domains/services/feature/src/lib/hooks/use-service/use-service.ts b/libs/domains/services/feature/src/lib/hooks/use-service/use-service.ts
index 4c215c8bd1a..135821bafa2 100644
--- a/libs/domains/services/feature/src/lib/hooks/use-service/use-service.ts
+++ b/libs/domains/services/feature/src/lib/hooks/use-service/use-service.ts
@@ -1,5 +1,7 @@
import { type UseQueryResult, useQuery } from '@tanstack/react-query'
import {
+ type AgenticWorkflow,
+ type AgenticWorkflowType,
type AnyService,
type Application,
type ApplicationType,
@@ -55,23 +57,25 @@ export function useService<
T extends ServiceType,
R = T extends ApplicationType
? Application
- : T extends ArgoCdType
- ? ArgoCd
- : T extends ContainerType
- ? Container
- : T extends DatabaseType
- ? Database
- : T extends JobType
- ? Job
- : T extends 'CRON_JOB'
+ : T extends AgenticWorkflowType
+ ? AgenticWorkflow
+ : T extends ArgoCdType
+ ? ArgoCd
+ : T extends ContainerType
+ ? Container
+ : T extends DatabaseType
+ ? Database
+ : T extends JobType
? Job
- : T extends 'LIFECYCLE_JOB'
+ : T extends 'CRON_JOB'
? Job
- : T extends HelmType
- ? Helm
- : T extends TerraformType
- ? Terraform
- : never,
+ : T extends 'LIFECYCLE_JOB'
+ ? Job
+ : T extends HelmType
+ ? Helm
+ : T extends TerraformType
+ ? Terraform
+ : never,
>(props: { serviceId: string; serviceType: T; suspense?: boolean }): UseQueryResult
export function useService({ serviceId, suspense = false, ...props }: UseServiceProps) {
const { data: serviceType } = useServiceType({
diff --git a/libs/domains/services/feature/src/lib/service-avatar/service-avatar.tsx b/libs/domains/services/feature/src/lib/service-avatar/service-avatar.tsx
index 284ddabc4f5..c9917b5a2e5 100644
--- a/libs/domains/services/feature/src/lib/service-avatar/service-avatar.tsx
+++ b/libs/domains/services/feature/src/lib/service-avatar/service-avatar.tsx
@@ -34,10 +34,10 @@ export const ServiceAvatar = forwardRef, ServiceAvatar
.with({ serviceType: 'JOB', job_type: 'LIFECYCLE' }, () => 'LIFECYCLE_JOB')
.with({ serviceType: 'JOB', job_type: 'CRON' }, () => 'CRON_JOB')
.with({ serviceType: 'ARGOCD_APP' }, () => 'ARGOCD')
+ .with({ serviceType: 'AGENTIC_WORKFLOW' }, () => 'AGENTIC_WORKFLOW')
.with({ serviceType: 'APPLICATION' }, { serviceType: 'CONTAINER' }, () => 'APPLICATION')
.with({ serviceType: 'DATABASE' }, () => 'DATABASE')
.with({ serviceType: 'TERRAFORM' }, () => 'TERRAFORM')
- .with({ serviceType: 'AGENTIC_WORKFLOW' }, () => 'AGENTIC_WORKFLOW')
.exhaustive()
const serviceAvatar = ServiceIcons[service.icon_uri as IconURI]
diff --git a/libs/domains/services/feature/src/lib/service-blueprint-update-flow/blueprint-update-flow.tsx b/libs/domains/services/feature/src/lib/service-blueprint-update-flow/blueprint-update-flow.tsx
index e7904cbe520..f24b9387255 100644
--- a/libs/domains/services/feature/src/lib/service-blueprint-update-flow/blueprint-update-flow.tsx
+++ b/libs/domains/services/feature/src/lib/service-blueprint-update-flow/blueprint-update-flow.tsx
@@ -59,12 +59,12 @@ export function BlueprintUpdateFlow({
const { mutateAsync: deployBlueprint, isLoading: isDeployLoading } = useDeployBlueprint({
environmentId,
serviceId: service.id,
- serviceType: service.service_type,
+ serviceType: service.serviceType,
})
const { mutateAsync: updateBlueprint, isLoading: isUpdateLoading } = useUpdateBlueprint({
environmentId,
serviceId: service.id,
- serviceType: service.service_type,
+ serviceType: service.serviceType,
})
const [activeSection, setActiveSection] = useState(() =>
blueprintUpdate ? getFirstAvailableUpdateSection(blueprintUpdate) : 'required'
diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx
index 2494f20cf56..1addec9bb63 100644
--- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx
+++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx
@@ -80,13 +80,15 @@ export function isOutputComplete(output: AgenticWorkflowOutput) {
function AgenticWorkflowSection({
children,
headerAction,
+ icon,
iconName,
invalid,
section,
}: {
children: ReactNode
headerAction?: ReactNode
- iconName: IconName
+ icon?: ReactNode
+ iconName?: IconName
invalid?: boolean
section: AgenticWorkflowConfigurationSection
}) {
@@ -98,7 +100,7 @@ function AgenticWorkflowSection({
const headerContent = (
<>
-
+ {icon ?? (iconName ? : null)}
{title}
@@ -349,7 +351,11 @@ export function AgenticWorkflowConfiguration() {
-
+ }
+ invalid={sectionInvalid['ai-model']}
+ >
)
})
+ .with({ serviceType: 'AGENTIC_WORKFLOW' }, (workflow) => (
+
+
+
+ {workflow.name}
+
+
+
+ {workflow.enabled ? 'Enabled' : 'Disabled'}
+
+
+ ))
.otherwise(() => (
({
@@ -359,7 +370,13 @@ describe('ServiceHeader', () => {
})
const renderServiceHeader = (
- serviceId: 'application-mock' | 'database-mock' | 'job-mock' | 'argocd-mock' | 'terraform-mock'
+ serviceId:
+ | 'application-mock'
+ | 'database-mock'
+ | 'job-mock'
+ | 'argocd-mock'
+ | 'terraform-mock'
+ | 'agentic-workflow-mock'
) => renderWithProviders()
it('renders application details and git metadata', () => {
@@ -409,6 +426,16 @@ describe('ServiceHeader', () => {
expect(screen.getByRole('button', { name: /313a525/i })).toBeInTheDocument()
})
+ it('renders agentic workflow model and enabled state', () => {
+ renderServiceHeader('agentic-workflow-mock')
+
+ expect(screen.getByRole('heading', { name: 'Review pull requests' })).toBeInTheDocument()
+ const modelBadge = screen.getByText('Claude').parentElement
+ expect(modelBadge).toBeInTheDocument()
+ expect(screen.getByText('Enabled')).toBeInTheDocument()
+ expect(modelBadge?.querySelector('img')).toHaveAttribute('src', '/assets/ai-tools/claude.svg')
+ })
+
it('renders an up to date badge for a current blueprint service', () => {
mockUseBlueprintUpdate.mockReturnValue({
data: {
diff --git a/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.tsx b/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.tsx
index 35da9b144e4..d84cac9b58e 100644
--- a/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.tsx
+++ b/libs/domains/services/feature/src/lib/service-overview/service-header/service-header.tsx
@@ -8,7 +8,7 @@ import {
useCluster,
useClusterRunningStatusSocket,
} from '@qovery/domains/clusters/feature'
-import { type AnyService, isArgoCd } from '@qovery/domains/services/data-access'
+import { type AnyService, isAgenticWorkflow, isArgoCd } from '@qovery/domains/services/data-access'
import {
IconEnum,
ServiceTypeEnum,
@@ -95,6 +95,7 @@ function ServiceHeaderIdentity({ environment, service }: ServiceHeaderIdentityPr
const { data: cluster } = useCluster({ organizationId, clusterId: environment.cluster_id, suspense: true })
const isArgoCdService = isArgoCd(service)
+ const isAgenticWorkflowService = isAgenticWorkflow(service)
useClusterRunningStatusSocket({ organizationId, clusterId: environment.cluster_id })
@@ -150,7 +151,7 @@ function ServiceHeaderIdentity({ environment, service }: ServiceHeaderIdentityPr
{isArgoCdService ? (
- ) : (
+ ) : isAgenticWorkflowService ? null : (
)}
@@ -237,17 +238,19 @@ function ServiceHeaderMetadata({ service }: ServiceHeaderMetadataProps) {
const { organizationId = '', projectId = '', environmentId = '', serviceId = '' } = useParams({ strict: false })
const { data: masterCredentials } = useMasterCredentials({
serviceId,
- serviceType: service?.service_type,
+ serviceType: isAgenticWorkflow(service) ? undefined : service?.service_type,
})
const [, copyToClipboard] = useCopyToClipboard()
const blueprintId = 'blueprint_id' in service ? service.blueprint_id : undefined
- const gitRepository = match(service)
- .with({ serviceType: 'APPLICATION' }, ({ git_repository }) => git_repository)
- .with({ serviceType: 'JOB', source: P.when(isJobGitSource) }, ({ source }) => source.docker?.git_repository)
- .with({ serviceType: 'HELM', source: P.when(isHelmGitSource) }, ({ source }) => source.git?.git_repository)
- .with({ serviceType: 'TERRAFORM' }, ({ terraform_files_source }) => terraform_files_source?.git?.git_repository)
- .with({ serviceType: 'ARGOCD_APP' }, ({ git_repository }) => git_repository)
- .otherwise(() => undefined)
+ const gitRepository = isAgenticWorkflow(service)
+ ? undefined
+ : match(service)
+ .with({ serviceType: 'APPLICATION' }, ({ git_repository }) => git_repository)
+ .with({ serviceType: 'JOB', source: P.when(isJobGitSource) }, ({ source }) => source.docker?.git_repository)
+ .with({ serviceType: 'HELM', source: P.when(isHelmGitSource) }, ({ source }) => source.git?.git_repository)
+ .with({ serviceType: 'TERRAFORM' }, ({ terraform_files_source }) => terraform_files_source?.git?.git_repository)
+ .with({ serviceType: 'ARGOCD_APP' }, ({ git_repository }) => git_repository)
+ .otherwise(() => undefined)
const containerImage = match(service)
.with({ serviceType: ServiceTypeEnum.JOB, source: P.when(isJobContainerSource) }, ({ source }) => source.image)
@@ -273,6 +276,7 @@ function ServiceHeaderMetadata({ service }: ServiceHeaderMetadataProps) {
.otherwise(() => undefined)
const isArgoCdService = isArgoCd(service)
+ const isAgenticWorkflowService = isAgenticWorkflow(service)
const handleCopyCredentials = (credentials: Credentials) => {
if (!databaseSource) {
@@ -382,6 +386,29 @@ function ServiceHeaderMetadata({ service }: ServiceHeaderMetadataProps) {
)}
>
)}
+ {isAgenticWorkflowService && (
+ <>
+