From 69eec4e41b8dbe87b1753ae970db7deb06e588e9 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Mon, 6 Oct 2025 15:56:21 +0800 Subject: [PATCH] Let copilot add external triggers feat: Enhance external trigger handling in Copilot - Added support for flexible schemas in external triggers, allowing configuration changes without stripping any data. - Introduced a new `onRequestTriggerSetup` callback in the Action component to facilitate trigger setup requests. - Implemented a modal for trigger configuration, improving user experience when setting up external triggers. - Updated the ComposioTriggerTypesPanel to auto-select trigger types based on initial configuration. This update significantly improves the management and setup of external triggers within the Copilot interface. --- apps/rowboat/app/lib/client_utils.ts | 4 + .../copilot/components/TriggerSetupModal.tsx | 237 ++++++++++++++++++ .../copilot/components/actions.tsx | 11 +- .../copilot/components/messages.tsx | 122 ++++++++- .../components/ComposioTriggerTypesPanel.tsx | 17 +- .../workflow/components/TriggerConfigForm.tsx | 30 ++- 6 files changed, 402 insertions(+), 19 deletions(-) create mode 100644 apps/rowboat/app/projects/[projectId]/copilot/components/TriggerSetupModal.tsx diff --git a/apps/rowboat/app/lib/client_utils.ts b/apps/rowboat/app/lib/client_utils.ts index c74ac367..2ea39915 100644 --- a/apps/rowboat/app/lib/client_utils.ts +++ b/apps/rowboat/app/lib/client_utils.ts @@ -93,6 +93,10 @@ export function validateConfigChanges(configType: string, configChanges: Record< }).passthrough(); break; } + case 'external_trigger': { + // External triggers have flexible schemas per provider; do not strip any config. + return { changes: configChanges }; + } default: return { error: `Unknown config type: ${configType}` }; } diff --git a/apps/rowboat/app/projects/[projectId]/copilot/components/TriggerSetupModal.tsx b/apps/rowboat/app/projects/[projectId]/copilot/components/TriggerSetupModal.tsx new file mode 100644 index 00000000..db49d714 --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/copilot/components/TriggerSetupModal.tsx @@ -0,0 +1,237 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Modal, ModalBody, ModalContent, ModalHeader } from '@heroui/react'; +import { z } from 'zod'; +import { ZToolkit } from '@/src/application/lib/composio/types'; +import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type'; +import { Project } from '@/src/entities/models/project'; +import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit'; +import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel'; +import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm'; +import { ToolkitAuthModal } from '../../tools/components/ToolkitAuthModal'; +import { fetchProject } from '@/app/actions/project.actions'; +import { createComposioTriggerDeployment } from '@/app/actions/composio.actions'; +import { Button, Spinner } from '@heroui/react'; + +interface TriggerSetupModalProps { + isOpen: boolean; + onClose: () => void; + projectId: string; + initialToolkitSlug?: string | null; + initialTriggerTypeSlug?: string | null; + initialTriggerConfig?: Record | null; + onCreated?: () => void; +} + +type Toolkit = z.infer; +type TriggerType = z.infer; +type ProjectConfig = z.infer; + +export function TriggerSetupModal({ + isOpen, + onClose, + projectId, + initialToolkitSlug = null, + initialTriggerTypeSlug = null, + initialTriggerConfig = null, + onCreated, +}: TriggerSetupModalProps) { + const [selectedToolkit, setSelectedToolkit] = useState(null); + const [selectedTriggerType, setSelectedTriggerType] = useState(null); + const [projectConfig, setProjectConfig] = useState(null); + const [showAuthModal, setShowAuthModal] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const [pendingTriggerTypeSlug, setPendingTriggerTypeSlug] = useState(null); + const [initialConfig, setInitialConfig] = useState | undefined>(); + + const loadProjectConfig = useCallback(async () => { + try { + const config = await fetchProject(projectId); + setProjectConfig(config); + } catch (err) { + console.error('Failed to fetch project configuration', err); + } + }, [projectId]); + + const resetState = useCallback(() => { + setSelectedToolkit(null); + setSelectedTriggerType(null); + setShowAuthModal(false); + setError(null); + setPendingTriggerTypeSlug(initialTriggerTypeSlug); + setInitialConfig(initialTriggerConfig ?? undefined); + }, [initialTriggerConfig, initialTriggerTypeSlug]); + + useEffect(() => { + if (!isOpen) { + return; + } + resetState(); + void loadProjectConfig(); + }, [isOpen, loadProjectConfig, resetState]); + + const requiresAuth = useMemo(() => { + if (!selectedToolkit) return false; + return !selectedToolkit.no_auth; + }, [selectedToolkit]); + + const hasActiveConnection = useMemo(() => { + if (!selectedToolkit) return false; + const status = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.status; + return status === 'ACTIVE'; + }, [projectConfig, selectedToolkit]); + + const handleSelectToolkit = useCallback((toolkit: Toolkit) => { + setSelectedToolkit(toolkit); + setSelectedTriggerType(null); + setError(null); + if (!initialToolkitSlug || toolkit.slug === initialToolkitSlug) { + setPendingTriggerTypeSlug(initialTriggerTypeSlug); + } else { + setPendingTriggerTypeSlug(null); + } + }, [initialToolkitSlug, initialTriggerTypeSlug]); + + const handleSelectTriggerType = useCallback((triggerType: TriggerType) => { + setSelectedTriggerType(triggerType); + setError(null); + setPendingTriggerTypeSlug(null); + if (requiresAuth && !hasActiveConnection) { + setShowAuthModal(true); + } + }, [requiresAuth, hasActiveConnection]); + + const handleAuthComplete = useCallback(async () => { + await loadProjectConfig(); + setShowAuthModal(false); + }, [loadProjectConfig]); + + const handleSubmit = useCallback(async (triggerConfig: Record) => { + if (!selectedToolkit || !selectedTriggerType) { + return; + } + + try { + setIsSubmitting(true); + setError(null); + + const connectedAccountId = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.id; + if (!connectedAccountId) { + setShowAuthModal(true); + throw new Error('Connect this toolkit before creating a trigger.'); + } + + await createComposioTriggerDeployment({ + projectId, + triggerTypeSlug: selectedTriggerType.slug, + connectedAccountId, + triggerConfig, + }); + + onCreated?.(); + onClose(); + } catch (err: any) { + console.error('Failed to create trigger', err); + setError(err?.message || 'Failed to create trigger. Please try again.'); + } finally { + setIsSubmitting(false); + } + }, [onClose, onCreated, projectConfig, projectId, selectedToolkit, selectedTriggerType]); + + const handleClose = useCallback(() => { + if (isSubmitting) { + return; + } + onClose(); + }, [isSubmitting, onClose]); + + return ( + <> + + + +

Set up External Trigger

+

+ Follow the guided flow to authenticate and configure the trigger. +

+
+ + {!selectedToolkit && ( + + )} + + {selectedToolkit && !selectedTriggerType && ( + setSelectedToolkit(null)} + onSelectTriggerType={handleSelectTriggerType} + initialTriggerTypeSlug={pendingTriggerTypeSlug} + /> + )} + + {selectedToolkit && selectedTriggerType && (!requiresAuth || hasActiveConnection) && ( +
+
+ +
+ setSelectedTriggerType(null)} + onSubmit={handleSubmit} + isSubmitting={isSubmitting} + initialConfig={initialConfig} + /> +
+ )} + + {selectedToolkit && selectedTriggerType && requiresAuth && !hasActiveConnection && !showAuthModal && ( +
+ +

+ Waiting for authentication to complete... +

+
+ )} + + {error && ( +
+ {error} +
+ )} +
+
+
+ + {selectedToolkit && ( + setShowAuthModal(false)} + projectId={projectId} + toolkitSlug={selectedToolkit.slug} + onComplete={handleAuthComplete} + /> + )} + + ); +} diff --git a/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx b/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx index 39483117..c9c25a8b 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx @@ -29,6 +29,7 @@ export function Action({ onApplied, externallyApplied = false, defaultExpanded = false, + onRequestTriggerSetup, }: { msgIndex: number; actionIndex: number; @@ -39,10 +40,12 @@ export function Action({ onApplied?: () => void; externallyApplied?: boolean; defaultExpanded?: boolean; + onRequestTriggerSetup?: (params: { action: z.infer['content']; msgIndex: number; actionIndex: number }) => void; }) { const { showPreview } = usePreviewModal(); const [expanded, setExpanded] = useState(defaultExpanded); const [appliedChanges, setAppliedChanges] = useState>({}); + const isExternalTriggerCreate = action.config_type === 'external_trigger' && action.action === 'create_new'; if (!action || typeof action !== 'object') { console.warn('Invalid action object:', action); @@ -108,6 +111,10 @@ export function Action({ // Handle applying all changes - delegate to parent const handleApplyAll = () => { + if (isExternalTriggerCreate) { + onRequestTriggerSetup?.({ action, msgIndex, actionIndex }); + return; + } // Mark all fields as applied locally for UI state const appliedKeys = Object.keys(action.config_changes).reduce((acc, key) => { acc[getAppliedChangeKey(msgIndex, actionIndex, key)] = true; @@ -230,9 +237,9 @@ export function Action({ onClick={() => handleApplyAll()} > - {allApplied ? 'Applied' : 'Apply'} + {allApplied ? 'Applied' : isExternalTriggerCreate ? 'Open setup' : 'Apply'} - {action.action !== 'delete' &&