Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
35dc103
feat(agent-task): add integration templates
RemiBonnet Sep 11, 2026
9b12742
fix(agent-task): complete coding template access
RemiBonnet Sep 11, 2026
ca46d1a
fix(agent-task): use official Linear logo
RemiBonnet Sep 11, 2026
c0d5afe
fix(agent-task): use official Jira logo
RemiBonnet Sep 11, 2026
c1df6ba
fix(agent-task): use official incident.io logo
RemiBonnet Sep 11, 2026
1a47045
feat(agent-task): show use case descriptions
RemiBonnet Sep 11, 2026
93b1ec5
feat(agent-task): refine use case cards
RemiBonnet Sep 11, 2026
3d45208
fix(agent-task): normalize card description weight
RemiBonnet Sep 11, 2026
b187c15
feat(agent-task): label use cases as beta
RemiBonnet Sep 11, 2026
0e4b865
fix(agent-task): position beta badge after title
RemiBonnet Sep 11, 2026
d04bbc0
fix(agent-task): use brand beta badge
RemiBonnet Sep 11, 2026
a3bd208
fix(agent-task): refine beta badge styling
RemiBonnet Sep 11, 2026
cffa7cc
fix(agent-task): reorder use case templates
RemiBonnet Sep 11, 2026
c568f8d
fix(agent-task): replace Linear logo asset
RemiBonnet Sep 11, 2026
38fdb0e
fix(agent-task): support themed Linear logo
RemiBonnet Sep 11, 2026
daf2245
feat(agent-task): add manual trigger action
RemiBonnet Sep 14, 2026
160c11f
fix(agent-task): refine trigger action
RemiBonnet Sep 14, 2026
0423a5f
fix(agent-task): expose trigger in service header
RemiBonnet Sep 14, 2026
462dd0b
fix(agent-task): align use case card spacing
RemiBonnet Sep 14, 2026
272b1a0
Revert "fix(agent-task): align use case card spacing"
RemiBonnet Sep 14, 2026
1152557
fix(agent-task): emphasize beta badge
RemiBonnet Sep 14, 2026
17a5dcc
fix(agent-task): address review feedback
RemiBonnet Sep 14, 2026
bdb0dde
refactor(agent-task): clarify icon resizing
RemiBonnet Sep 14, 2026
3118480
fix(agent-task): normalize use case icon frames
RemiBonnet Sep 14, 2026
11591d9
refactor(agent-task): use generated deploy endpoint
RemiBonnet Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/console/public/assets/agent-templates/honeybadger.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions apps/console/public/assets/agent-templates/incident-io.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions apps/console/public/assets/agent-templates/jira.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions apps/console/public/assets/agent-templates/linear-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions apps/console/public/assets/agent-templates/linear-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,10 @@ type EditAdvancedSettingsRequest = {
}

export const mutations = {
async deployAgenticWorkflow({ agenticWorkflowId }: { agenticWorkflowId: string }) {
const response = await agenticWorkflowsApi.deployAgenticWorkflow(agenticWorkflowId)
return response.data
},
async cloneService({ serviceId, serviceType, payload }: CloneServiceRequest) {
const { mutation } = match(serviceType)
.with('APPLICATION', (serviceType) => ({
Expand Down
1 change: 1 addition & 0 deletions libs/domains/services/feature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export * from './lib/hooks/use-restart-all-services/use-restart-all-services'
export * from './lib/hooks/use-restart-service/use-restart-service'
export * from './lib/hooks/use-deploy-all-services/use-deploy-all-services'
export * from './lib/hooks/use-deploy-service/use-deploy-service'
export * from './lib/hooks/use-deploy-agentic-workflow/use-deploy-agentic-workflow'
export * from './lib/hooks/use-redeploy-service-action/use-redeploy-service-action'
export * from './lib/hooks/use-stop-all-services/use-stop-all-services'
export * from './lib/hooks/use-uninstall-all-services/use-uninstall-all-services'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { AgenticWorkflowServiceActions } from './agentic-workflow-service-actions'

const mockDeleteService = jest.fn()
const mockDeployAgenticWorkflow = jest.fn()
const mockNavigate = jest.fn()
const mockOpenModalConfirmation = jest.fn()
const mockCopyToClipboard = jest.fn()
const mockAuditLogsLink = jest.fn()
let mockIsDeploying = false

jest.mock('@tanstack/react-router', () => ({
useNavigate: () => mockNavigate,
Expand Down Expand Up @@ -42,6 +44,10 @@ jest.mock('../hooks/use-delete-service/use-delete-service', () => ({
useDeleteService: () => ({ mutateAsync: mockDeleteService }),
}))

jest.mock('../hooks/use-deploy-agentic-workflow/use-deploy-agentic-workflow', () => ({
useDeployAgenticWorkflow: () => ({ mutate: mockDeployAgenticWorkflow, isLoading: mockIsDeploying }),
}))

jest.mock('@qovery/shared/util-hooks', () => ({
useCopyToClipboard: () => [undefined, mockCopyToClipboard],
}))
Expand All @@ -62,6 +68,7 @@ describe('AgenticWorkflowServiceActions', () => {

beforeEach(() => {
jest.clearAllMocks()
mockIsDeploying = false
})

it('only exposes audit logs, metadata, and delete actions', async () => {
Expand All @@ -87,6 +94,39 @@ describe('AgenticWorkflowServiceActions', () => {
expect(screen.queryByRole('menuitem', { name: 'Redeploy' })).not.toBeInTheDocument()
})

it('triggers the workflow from the button to the left of the actions menu', async () => {
const { userEvent } = renderWithProviders(
<AgenticWorkflowServiceActions environment={environment} service={service} />
)

const triggerButton = screen.getByRole('button', { name: 'Trigger' })
const actionsButton = screen.getByRole('button', { name: 'Other actions for Review pull requests' })

expect(triggerButton.compareDocumentPosition(actionsButton)).toBe(Node.DOCUMENT_POSITION_FOLLOWING)

await userEvent.hover(triggerButton)

expect(await screen.findByRole('tooltip')).toHaveTextContent('Trigger')

await userEvent.click(triggerButton)

expect(mockDeployAgenticWorkflow).toHaveBeenCalledWith({ agenticWorkflowId: 'workflow-1' })
})

it('shows the trigger button in the service header', () => {
renderWithProviders(<AgenticWorkflowServiceActions environment={environment} service={service} variant="header" />)

expect(screen.getByRole('button', { name: 'Trigger' })).toBeInTheDocument()
})

it('disables the trigger button while a request is pending', () => {
mockIsDeploying = true

renderWithProviders(<AgenticWorkflowServiceActions environment={environment} service={service} />)

expect(screen.getByRole('button', { name: 'Trigger' })).toBeDisabled()
})

it('shows and copies service metadata', async () => {
const { userEvent } = renderWithProviders(
<AgenticWorkflowServiceActions environment={environment} service={service} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { type AgenticWorkflow } from '@qovery/domains/services/data-access'
import { Button, DropdownMenu, Icon, Tooltip, useModalConfirmation } from '@qovery/shared/ui'
import { useCopyToClipboard } from '@qovery/shared/util-hooks'
import { useDeleteService } from '../hooks/use-delete-service/use-delete-service'
import { useDeployAgenticWorkflow } from '../hooks/use-deploy-agentic-workflow/use-deploy-agentic-workflow'

export interface AgenticWorkflowServiceActionsProps {
environment: Environment
Expand All @@ -25,6 +26,10 @@ export function AgenticWorkflowServiceActions({
const navigate = useNavigate()
const { openModalConfirmation } = useModalConfirmation()
const { mutateAsync: deleteService } = useDeleteService({ organizationId, environmentId })
const { mutate: deployAgenticWorkflow, isLoading: isDeploying } = useDeployAgenticWorkflow({
environmentId,
serviceId: service.id,
})
const [copiedMetadataLabel, setCopiedMetadataLabel] = useState<string>()
const [, copyToClipboard] = useCopyToClipboard()
const metadata = [
Expand Down Expand Up @@ -57,7 +62,21 @@ export function AgenticWorkflowServiceActions({
}

return (
<div onClick={onAction}>
<div className="flex items-center gap-2" onClick={onAction}>
<Tooltip content="Trigger">
<Button
aria-label="Trigger"
variant="outline"
size={variant === 'header' ? 'md' : 'sm'}
iconOnly
loading={isDeploying}
disabled={isDeploying}
onClick={() => deployAgenticWorkflow({ agenticWorkflowId: service.id })}
onKeyDown={onAction}
>
<Icon iconName="play" />
</Button>
</Tooltip>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { mutations } from '@qovery/domains/services/data-access'
import { toast } from '@qovery/shared/ui'
import { queries } from '@qovery/state/util-queries'

export function useDeployAgenticWorkflow({ environmentId, serviceId }: { environmentId: string; serviceId: string }) {
const queryClient = useQueryClient()

return useMutation(mutations.deployAgenticWorkflow, {
onSuccess() {
queryClient.invalidateQueries({
queryKey: queries.services.listStatuses(environmentId).queryKey,
})
queryClient.invalidateQueries({
queryKey: queries.services.status({ id: serviceId, serviceType: 'AGENTIC_WORKFLOW' }).queryKey,
})
queryClient.invalidateQueries({
queryKey: queries.services.deploymentHistory({ serviceId, serviceType: 'AGENTIC_WORKFLOW' }).queryKey,
})
queryClient.invalidateQueries({
queryKey: queries.environments.deploymentHistoryV2({ environmentId }).queryKey,
})

toast('success', 'Agent task triggered')
},
meta: {
notifyOnError: true,
},
})
}

export default useDeployAgenticWorkflow
Original file line number Diff line number Diff line change
@@ -1,22 +1,56 @@
import { AgenticWorkflowExecutionMode } from 'qovery-typescript-axios'
import { AGENTIC_WORKFLOW_TEMPLATES, getAgenticWorkflowTemplate } from './agentic-workflow-templates'

describe('agentic-workflow-templates', () => {
it('exposes the Incident Analyser use case', () => {
const template = getAgenticWorkflowTemplate('incident-analyser')
expect(template).toBeDefined()
expect(template?.title).toBe('Incident Analyser')
expect(template?.seed.name).toBe('Incident Analyser')
expect(template?.seed.agentPrompt).toBeTruthy()
// The prompt must reference the seeded credentials so the agent actually uses them.
expect(template?.seed.agentPrompt).toContain('INCIDENT_IO_API_KEY')
expect(template?.seed.agentPrompt).toContain('SLACK_WEBHOOK_URL')
expect(template?.seed.cpu).toBe('200')
expect(template?.seed.memory).toBe('256')
it.each([
['incident-io-analyzer', 'INCIDENT_IO_API_KEY'],
['honeybadger-incident-analyzer', 'HONEYBADGER_API_TOKEN'],
])('preconfigures the %s incident analyzer', (id, credential) => {
const template = getAgenticWorkflowTemplate(id)

expect(template?.logoPath).toBeTruthy()
expect(template?.seed.executionMode).toBe(AgenticWorkflowExecutionMode.CLONE_ENVIRONMENT)
expect(template?.seed.agentPrompt).toContain(credential)
expect(template?.seed.automations).toEqual([
expect.objectContaining({ triggers: [expect.objectContaining({ type: 'webhook' })] }),
])
expect(template?.variables?.map((variable) => variable.variable)).toContain(credential)
})

it('configures Jira Cloud Basic authentication and tenant access', () => {
const template = getAgenticWorkflowTemplate('jira-coding-agent')

expect(template?.seed.agentPrompt).toContain('JIRA_EMAIL')
expect(template?.seed.agentPrompt).toContain('HTTP Basic auth')
expect(template?.seed.whitelistHosts?.split(',')).toContain('*.atlassian.net')
expect(template?.variables?.map((variable) => variable.variable)).toEqual([
'INCIDENT_IO_API_KEY',
'SLACK_WEBHOOK_URL',
'JIRA_BASE_URL',
'JIRA_EMAIL',
'JIRA_API_TOKEN',
])
expect(template?.variables?.every((variable) => variable.isSecret)).toBe(true)
})

it.each(['jira-coding-agent', 'linear-coding-agent'])('allows the %s to reach the Bitbucket API', (id) => {
expect(getAgenticWorkflowTemplate(id)?.seed.whitelistHosts?.split(',')).toContain('api.bitbucket.org')
})

it.each([
['jira-coding-agent', 'Jira Coding Agent', 'JIRA_API_TOKEN'],
['linear-coding-agent', 'Linear Coding Agent', 'LINEAR_API_KEY'],
])('exposes the %s template', (id, title, credential) => {
const template = getAgenticWorkflowTemplate(id)

expect(template?.title).toBe(title)
expect(template?.logoPath).toBeTruthy()
expect(template?.seed.agentPrompt).toContain(credential)
expect(template?.variables?.map((variable) => variable.variable)).toContain(credential)
})

it('provides theme-specific Linear logos', () => {
const template = getAgenticWorkflowTemplate('linear-coding-agent')

expect(template?.logoPath).toBe('/assets/agent-templates/linear-dark.svg')
expect(template?.darkLogoPath).toBe('/assets/agent-templates/linear-light.svg')
})

it('exposes the Build & deployment optimizer use case', () => {
Expand All @@ -42,7 +76,17 @@ describe('agentic-workflow-templates', () => {
const ids = AGENTIC_WORKFLOW_TEMPLATES.map((template) => template.id)
expect(new Set(ids).size).toBe(ids.length)
AGENTIC_WORKFLOW_TEMPLATES.forEach((template) => {
expect(template.iconName).toBeTruthy()
expect(template.iconName ?? template.logoPath).toBeTruthy()
})
})

it('places the build optimizer before coding agents', () => {
expect(AGENTIC_WORKFLOW_TEMPLATES.map((template) => template.id)).toEqual([
'incident-io-analyzer',
'honeybadger-incident-analyzer',
'build-optimizer',
'jira-coding-agent',
'linear-coding-agent',
])
})
})
Loading
Loading