From 651a998fbb1cd654b7d40c3c3e98d54f8bf7c70d Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 25 Sep 2025 14:55:04 +0530 Subject: [PATCH] feat: Enhance Copilot message handling and trigger actions - Pass projectId to Messages and AssistantMessage components for better context - Refactor applyAction to handle one-time and recurring triggers with improved error handling - Update handleApplyAll and handleSingleApply to support async action processing - Remove deprecated pending trigger logic from workflow editor This update improves the Copilot's ability to manage triggers and enhances the overall message processing flow. --- .../app/projects/[projectId]/copilot/app.tsx | 1 + .../copilot/components/messages.tsx | 167 +++++++++++------- .../[projectId]/workflow/workflow_editor.tsx | 87 --------- .../components/common/compose-box-copilot.tsx | 2 +- 4 files changed, 106 insertions(+), 151 deletions(-) diff --git a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx index ca765952..6af50863 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx @@ -255,6 +255,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message )} ['content'], workflow: z.infer, dispatch: (action: any) => void, messageIndex: number, loading: boolean, - onStatusBarChange?: (status: any) => void + onStatusBarChange?: (status: any) => void; + projectId: string; }) { const blocks = useParsedBlocks(content); const [appliedActions, setAppliedActions] = useState>(new Set()); @@ -208,14 +210,13 @@ function AssistantMessage({ const allApplied = pendingCount === 0 && totalActions > 0; // Memoized applyAction for useCallback dependencies - const applyAction = useCallback((action: any, actionIndex: number) => { - // Only apply, do not update appliedActions here + const applyAction = useCallback(async (action: any, actionIndex: number): Promise => { if (action.action === 'create_new') { switch (action.config_type) { case 'agent': { // Prevent duplicate agent names if (workflow.agents.some((agent: any) => agent.name === action.name)) { - return; + return false; } dispatch({ type: 'add_agent', @@ -225,12 +226,12 @@ function AssistantMessage({ }, fromCopilot: true }); - break; + return true; } case 'tool': { // Prevent duplicate tool names if (workflow.tools.some((tool: any) => tool.name === action.name)) { - return; + return false; } dispatch({ type: 'add_tool', @@ -240,7 +241,7 @@ function AssistantMessage({ }, fromCopilot: true }); - break; + return true; } case 'prompt': dispatch({ @@ -251,7 +252,7 @@ function AssistantMessage({ }, fromCopilot: true }); - break; + return true; case 'pipeline': dispatch({ type: 'add_pipeline', @@ -261,27 +262,45 @@ function AssistantMessage({ }, fromCopilot: true }); - break; - case 'one_time_trigger': - dispatch({ - type: 'add_one_time_trigger', - trigger: { - name: action.name, - ...action.config_changes - }, - fromCopilot: true - }); - break; - case 'recurring_trigger': - dispatch({ - type: 'add_recurring_trigger', - trigger: { - name: action.name, - ...action.config_changes - }, - fromCopilot: true - }); - break; + return true; + case 'one_time_trigger': { + const { scheduledTime, input } = action.config_changes || {}; + if (!scheduledTime || !input) { + console.error('Missing scheduledTime or input for one-time trigger', action); + return false; + } + try { + const { createScheduledJobRule } = await import('@/app/actions/scheduled-job-rules.actions'); + await createScheduledJobRule({ + projectId, + scheduledTime, + input, + }); + return true; + } catch (error) { + console.error('Failed to create one-time trigger', error); + return false; + } + } + case 'recurring_trigger': { + const { cron, input } = action.config_changes || {}; + if (!cron || !input) { + console.error('Missing cron or input for recurring trigger', action); + return false; + } + try { + const { createRecurringJobRule } = await import('@/app/actions/recurring-job-rules.actions'); + await createRecurringJobRule({ + projectId, + cron, + input, + }); + return true; + } catch (error) { + console.error('Failed to create recurring trigger', error); + return false; + } + } } } else if (action.action === 'edit') { switch (action.config_type) { @@ -291,34 +310,34 @@ function AssistantMessage({ name: action.name, agent: action.config_changes }); - break; + return true; case 'tool': dispatch({ type: 'update_tool_no_select', name: action.name, tool: action.config_changes }); - break; + return true; case 'prompt': dispatch({ type: 'update_prompt', name: action.name, prompt: action.config_changes }); - break; + return true; case 'pipeline': dispatch({ type: 'update_pipeline', name: action.name, pipeline: action.config_changes }); - break; + return true; case 'start_agent': dispatch({ type: 'set_main_agent', name: action.name, - }) - break; + }); + return true; } } else if (action.action === 'delete') { switch (action.config_type) { @@ -327,61 +346,80 @@ function AssistantMessage({ type: 'delete_agent', name: action.name }); - break; + return true; case 'tool': dispatch({ type: 'delete_tool', name: action.name }); - break; + return true; case 'prompt': dispatch({ type: 'delete_prompt', name: action.name }); - break; + return true; case 'pipeline': dispatch({ type: 'delete_pipeline', name: action.name }); - break; + return true; } } - }, [dispatch, workflow.agents, workflow.tools]); + + console.warn('Unhandled action from Copilot applyAction', action, actionIndex); + return false; + }, [dispatch, projectId, workflow.agents, workflow.tools]); // Memoized handleApplyAll for useEffect dependencies - const handleApplyAll = useCallback(() => { - // Find all unapplied action indices + const handleApplyAll = useCallback(async () => { const unapplied = parsed .map((part, idx) => ({ part, actionIndex: idx })) .filter(({ part, actionIndex }) => part.type === 'action' && !appliedActions.has(actionIndex)) - .map(({ part, actionIndex }) => ({ - action: part.type === 'action' ? part.action : null, - actionIndex + .map(({ part, actionIndex }) => ({ + action: part.type === 'action' ? part.action : null, + actionIndex, })) .filter(({ action }) => action !== null); - // Synchronously apply all unapplied actions - unapplied.forEach(({ action, actionIndex }) => { - applyAction(action, actionIndex); - }); + const newlyApplied: number[] = []; - // After all are applied, update the state in one go - setAppliedActions(prev => { - const next = new Set(prev); - unapplied.forEach(({ actionIndex }) => next.add(actionIndex)); - return next; - }); - }, [parsed, appliedActions, setAppliedActions, applyAction]); + for (const { action, actionIndex } of unapplied) { + try { + const success = await applyAction(action, actionIndex); + if (success) { + newlyApplied.push(actionIndex); + } + } catch (error) { + console.error('Failed to apply Copilot action', action, error); + } + } + + if (newlyApplied.length > 0) { + setAppliedActions(prev => { + const next = new Set(prev); + newlyApplied.forEach(index => next.add(index)); + return next; + }); + } + }, [parsed, appliedActions, applyAction]); // Manual single apply (from card) - const handleSingleApply = (action: any, actionIndex: number) => { - if (!appliedActions.has(actionIndex)) { - applyAction(action, actionIndex); - setAppliedActions(prev => new Set([...prev, actionIndex])); + const handleSingleApply = useCallback(async (action: any, actionIndex: number) => { + if (appliedActions.has(actionIndex)) { + return; } - }; + + try { + const success = await applyAction(action, actionIndex); + if (success) { + setAppliedActions(prev => new Set([...prev, actionIndex])); + } + } catch (error) { + console.error('Failed to apply Copilot action', action, error); + } + }, [appliedActions, applyAction]); useEffect(() => { if (loading) { @@ -481,7 +519,7 @@ function AssistantMessage({ workflow={workflow} dispatch={dispatch} stale={false} - onApplied={() => handleSingleApply(part.action, idx)} + onApplied={() => { void handleSingleApply(part.action, idx); }} externallyApplied={appliedActions.has(idx)} defaultExpanded={true} /> @@ -526,6 +564,7 @@ function AssistantMessageLoading({ currentStatus }: { currentStatus: 'thinking' } export function Messages({ + projectId, messages, streamingResponse, loadingResponse, @@ -535,6 +574,7 @@ export function Messages({ toolCalling, toolQuery }: { + projectId: string; messages: z.infer[]; streamingResponse: string; loadingResponse: boolean; @@ -584,6 +624,7 @@ export function Messages({ dispatch={dispatch} messageIndex={messageIndex} loading={loadingResponse} + projectId={projectId} onStatusBarChange={status => { // Only update for the last assistant message if (messageIndex === displayMessages.length - 1) { diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index 4f70fe98..476d8366 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -103,15 +103,6 @@ interface StateItem { lastUpdatedAt: string; isLive: boolean; agentInstructionsChanged: boolean; - pendingTriggers?: Array<{ - type: 'one_time' | 'recurring'; - name: string; - scheduledTime?: string; - cron?: string; - input: { - messages: z.infer[]; - }; - }>; } interface State { @@ -153,28 +144,6 @@ export type Action = { pipeline: Partial>; defaultModel?: string; fromCopilot?: boolean; -} | { - type: "add_one_time_trigger"; - trigger: { - name: string; - scheduledTime: string; - input: { - messages: z.infer[]; - }; - }; - fromCopilot?: boolean; -} | { - type: "add_recurring_trigger"; - trigger: { - name: string; - cron: string; - input: { - messages: z.infer[]; - }; - }; - fromCopilot?: boolean; -} | { - type: "clear_pending_triggers"; } | { type: "select_agent"; name: string; @@ -618,30 +587,6 @@ function reducer(state: State, action: Action): State { draft.chatKey++; break; } - case "add_one_time_trigger": { - // Mark that we need to create a trigger - the actual API call will be handled outside the reducer - draft.pendingTriggers = draft.pendingTriggers || []; - draft.pendingTriggers.push({ - type: 'one_time', - ...action.trigger - }); - draft.pendingChanges = true; - break; - } - case "add_recurring_trigger": { - // Mark that we need to create a trigger - the actual API call will be handled outside the reducer - draft.pendingTriggers = draft.pendingTriggers || []; - draft.pendingTriggers.push({ - type: 'recurring', - ...action.trigger - }); - draft.pendingChanges = true; - break; - } - case "clear_pending_triggers": { - draft.pendingTriggers = []; - break; - } case "delete_agent": // Remove the agent draft.workflow.agents = draft.workflow.agents.filter( @@ -1370,38 +1315,6 @@ export function WorkflowEditor({ } }, [state.present.agentInstructionsChanged, markAgentInstructionsChanged]); - // Handle pending trigger creation - useEffect(() => { - if (state.present.pendingTriggers && state.present.pendingTriggers.length > 0) { - const processTriggers = async () => { - for (const trigger of state.present.pendingTriggers!) { - try { - if (trigger.type === 'one_time') { - const { createScheduledJobRule } = await import('@/app/actions/scheduled-job-rules.actions'); - await createScheduledJobRule({ - projectId, - input: trigger.input, - scheduledTime: trigger.scheduledTime! - }); - } else if (trigger.type === 'recurring') { - const { createRecurringJobRule } = await import('@/app/actions/recurring-job-rules.actions'); - await createRecurringJobRule({ - projectId, - input: trigger.input, - cron: trigger.cron! - }); - } - } catch (error) { - console.error(`Failed to create ${trigger.type} trigger:`, error); - } - } - // Clear pending triggers after processing - dispatch({ type: "clear_pending_triggers" }); - }; - processTriggers(); - } - }, [state.present.pendingTriggers, projectId]); - function handleSelectAgent(name: string) { dispatch({ type: "select_agent", name }); } diff --git a/apps/rowboat/components/common/compose-box-copilot.tsx b/apps/rowboat/components/common/compose-box-copilot.tsx index d2ddd4d2..22c12503 100644 --- a/apps/rowboat/components/common/compose-box-copilot.tsx +++ b/apps/rowboat/components/common/compose-box-copilot.tsx @@ -268,7 +268,7 @@ function CopilotStatusBar({ // Show real button when ready return (