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.
This commit is contained in:
tusharmagar 2025-09-25 14:55:04 +05:30
parent 24f930fa86
commit 651a998fbb
4 changed files with 106 additions and 151 deletions

View file

@ -255,6 +255,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
</div> </div>
)} )}
<Messages <Messages
projectId={projectId}
messages={messages} messages={messages}
streamingResponse={streamingResponse} streamingResponse={streamingResponse}
loadingResponse={loadingResponse} loadingResponse={loadingResponse}

View file

@ -171,14 +171,16 @@ function AssistantMessage({
dispatch, dispatch,
messageIndex, messageIndex,
loading, loading,
onStatusBarChange onStatusBarChange,
projectId,
}: { }: {
content: z.infer<typeof CopilotAssistantMessage>['content'], content: z.infer<typeof CopilotAssistantMessage>['content'],
workflow: z.infer<typeof Workflow>, workflow: z.infer<typeof Workflow>,
dispatch: (action: any) => void, dispatch: (action: any) => void,
messageIndex: number, messageIndex: number,
loading: boolean, loading: boolean,
onStatusBarChange?: (status: any) => void onStatusBarChange?: (status: any) => void;
projectId: string;
}) { }) {
const blocks = useParsedBlocks(content); const blocks = useParsedBlocks(content);
const [appliedActions, setAppliedActions] = useState<Set<number>>(new Set()); const [appliedActions, setAppliedActions] = useState<Set<number>>(new Set());
@ -208,14 +210,13 @@ function AssistantMessage({
const allApplied = pendingCount === 0 && totalActions > 0; const allApplied = pendingCount === 0 && totalActions > 0;
// Memoized applyAction for useCallback dependencies // Memoized applyAction for useCallback dependencies
const applyAction = useCallback((action: any, actionIndex: number) => { const applyAction = useCallback(async (action: any, actionIndex: number): Promise<boolean> => {
// Only apply, do not update appliedActions here
if (action.action === 'create_new') { if (action.action === 'create_new') {
switch (action.config_type) { switch (action.config_type) {
case 'agent': { case 'agent': {
// Prevent duplicate agent names // Prevent duplicate agent names
if (workflow.agents.some((agent: any) => agent.name === action.name)) { if (workflow.agents.some((agent: any) => agent.name === action.name)) {
return; return false;
} }
dispatch({ dispatch({
type: 'add_agent', type: 'add_agent',
@ -225,12 +226,12 @@ function AssistantMessage({
}, },
fromCopilot: true fromCopilot: true
}); });
break; return true;
} }
case 'tool': { case 'tool': {
// Prevent duplicate tool names // Prevent duplicate tool names
if (workflow.tools.some((tool: any) => tool.name === action.name)) { if (workflow.tools.some((tool: any) => tool.name === action.name)) {
return; return false;
} }
dispatch({ dispatch({
type: 'add_tool', type: 'add_tool',
@ -240,7 +241,7 @@ function AssistantMessage({
}, },
fromCopilot: true fromCopilot: true
}); });
break; return true;
} }
case 'prompt': case 'prompt':
dispatch({ dispatch({
@ -251,7 +252,7 @@ function AssistantMessage({
}, },
fromCopilot: true fromCopilot: true
}); });
break; return true;
case 'pipeline': case 'pipeline':
dispatch({ dispatch({
type: 'add_pipeline', type: 'add_pipeline',
@ -261,27 +262,45 @@ function AssistantMessage({
}, },
fromCopilot: true fromCopilot: true
}); });
break; return true;
case 'one_time_trigger': case 'one_time_trigger': {
dispatch({ const { scheduledTime, input } = action.config_changes || {};
type: 'add_one_time_trigger', if (!scheduledTime || !input) {
trigger: { console.error('Missing scheduledTime or input for one-time trigger', action);
name: action.name, return false;
...action.config_changes }
}, try {
fromCopilot: true const { createScheduledJobRule } = await import('@/app/actions/scheduled-job-rules.actions');
await createScheduledJobRule({
projectId,
scheduledTime,
input,
}); });
break; return true;
case 'recurring_trigger': } catch (error) {
dispatch({ console.error('Failed to create one-time trigger', error);
type: 'add_recurring_trigger', return false;
trigger: { }
name: action.name, }
...action.config_changes case 'recurring_trigger': {
}, const { cron, input } = action.config_changes || {};
fromCopilot: true 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,
}); });
break; return true;
} catch (error) {
console.error('Failed to create recurring trigger', error);
return false;
}
}
} }
} else if (action.action === 'edit') { } else if (action.action === 'edit') {
switch (action.config_type) { switch (action.config_type) {
@ -291,34 +310,34 @@ function AssistantMessage({
name: action.name, name: action.name,
agent: action.config_changes agent: action.config_changes
}); });
break; return true;
case 'tool': case 'tool':
dispatch({ dispatch({
type: 'update_tool_no_select', type: 'update_tool_no_select',
name: action.name, name: action.name,
tool: action.config_changes tool: action.config_changes
}); });
break; return true;
case 'prompt': case 'prompt':
dispatch({ dispatch({
type: 'update_prompt', type: 'update_prompt',
name: action.name, name: action.name,
prompt: action.config_changes prompt: action.config_changes
}); });
break; return true;
case 'pipeline': case 'pipeline':
dispatch({ dispatch({
type: 'update_pipeline', type: 'update_pipeline',
name: action.name, name: action.name,
pipeline: action.config_changes pipeline: action.config_changes
}); });
break; return true;
case 'start_agent': case 'start_agent':
dispatch({ dispatch({
type: 'set_main_agent', type: 'set_main_agent',
name: action.name, name: action.name,
}) });
break; return true;
} }
} else if (action.action === 'delete') { } else if (action.action === 'delete') {
switch (action.config_type) { switch (action.config_type) {
@ -327,61 +346,80 @@ function AssistantMessage({
type: 'delete_agent', type: 'delete_agent',
name: action.name name: action.name
}); });
break; return true;
case 'tool': case 'tool':
dispatch({ dispatch({
type: 'delete_tool', type: 'delete_tool',
name: action.name name: action.name
}); });
break; return true;
case 'prompt': case 'prompt':
dispatch({ dispatch({
type: 'delete_prompt', type: 'delete_prompt',
name: action.name name: action.name
}); });
break; return true;
case 'pipeline': case 'pipeline':
dispatch({ dispatch({
type: 'delete_pipeline', type: 'delete_pipeline',
name: action.name 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 // Memoized handleApplyAll for useEffect dependencies
const handleApplyAll = useCallback(() => { const handleApplyAll = useCallback(async () => {
// Find all unapplied action indices
const unapplied = parsed const unapplied = parsed
.map((part, idx) => ({ part, actionIndex: idx })) .map((part, idx) => ({ part, actionIndex: idx }))
.filter(({ part, actionIndex }) => part.type === 'action' && !appliedActions.has(actionIndex)) .filter(({ part, actionIndex }) => part.type === 'action' && !appliedActions.has(actionIndex))
.map(({ part, actionIndex }) => ({ .map(({ part, actionIndex }) => ({
action: part.type === 'action' ? part.action : null, action: part.type === 'action' ? part.action : null,
actionIndex actionIndex,
})) }))
.filter(({ action }) => action !== null); .filter(({ action }) => action !== null);
// Synchronously apply all unapplied actions const newlyApplied: number[] = [];
unapplied.forEach(({ action, actionIndex }) => {
applyAction(action, actionIndex);
});
// After all are applied, update the state in one go 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 => { setAppliedActions(prev => {
const next = new Set(prev); const next = new Set(prev);
unapplied.forEach(({ actionIndex }) => next.add(actionIndex)); newlyApplied.forEach(index => next.add(index));
return next; return next;
}); });
}, [parsed, appliedActions, setAppliedActions, applyAction]); }
}, [parsed, appliedActions, applyAction]);
// Manual single apply (from card) // Manual single apply (from card)
const handleSingleApply = (action: any, actionIndex: number) => { const handleSingleApply = useCallback(async (action: any, actionIndex: number) => {
if (!appliedActions.has(actionIndex)) { if (appliedActions.has(actionIndex)) {
applyAction(action, actionIndex); return;
}
try {
const success = await applyAction(action, actionIndex);
if (success) {
setAppliedActions(prev => new Set([...prev, actionIndex])); setAppliedActions(prev => new Set([...prev, actionIndex]));
} }
}; } catch (error) {
console.error('Failed to apply Copilot action', action, error);
}
}, [appliedActions, applyAction]);
useEffect(() => { useEffect(() => {
if (loading) { if (loading) {
@ -481,7 +519,7 @@ function AssistantMessage({
workflow={workflow} workflow={workflow}
dispatch={dispatch} dispatch={dispatch}
stale={false} stale={false}
onApplied={() => handleSingleApply(part.action, idx)} onApplied={() => { void handleSingleApply(part.action, idx); }}
externallyApplied={appliedActions.has(idx)} externallyApplied={appliedActions.has(idx)}
defaultExpanded={true} defaultExpanded={true}
/> />
@ -526,6 +564,7 @@ function AssistantMessageLoading({ currentStatus }: { currentStatus: 'thinking'
} }
export function Messages({ export function Messages({
projectId,
messages, messages,
streamingResponse, streamingResponse,
loadingResponse, loadingResponse,
@ -535,6 +574,7 @@ export function Messages({
toolCalling, toolCalling,
toolQuery toolQuery
}: { }: {
projectId: string;
messages: z.infer<typeof CopilotMessage>[]; messages: z.infer<typeof CopilotMessage>[];
streamingResponse: string; streamingResponse: string;
loadingResponse: boolean; loadingResponse: boolean;
@ -584,6 +624,7 @@ export function Messages({
dispatch={dispatch} dispatch={dispatch}
messageIndex={messageIndex} messageIndex={messageIndex}
loading={loadingResponse} loading={loadingResponse}
projectId={projectId}
onStatusBarChange={status => { onStatusBarChange={status => {
// Only update for the last assistant message // Only update for the last assistant message
if (messageIndex === displayMessages.length - 1) { if (messageIndex === displayMessages.length - 1) {

View file

@ -103,15 +103,6 @@ interface StateItem {
lastUpdatedAt: string; lastUpdatedAt: string;
isLive: boolean; isLive: boolean;
agentInstructionsChanged: boolean; agentInstructionsChanged: boolean;
pendingTriggers?: Array<{
type: 'one_time' | 'recurring';
name: string;
scheduledTime?: string;
cron?: string;
input: {
messages: z.infer<typeof Message>[];
};
}>;
} }
interface State { interface State {
@ -153,28 +144,6 @@ export type Action = {
pipeline: Partial<z.infer<typeof WorkflowPipeline>>; pipeline: Partial<z.infer<typeof WorkflowPipeline>>;
defaultModel?: string; defaultModel?: string;
fromCopilot?: boolean; fromCopilot?: boolean;
} | {
type: "add_one_time_trigger";
trigger: {
name: string;
scheduledTime: string;
input: {
messages: z.infer<typeof Message>[];
};
};
fromCopilot?: boolean;
} | {
type: "add_recurring_trigger";
trigger: {
name: string;
cron: string;
input: {
messages: z.infer<typeof Message>[];
};
};
fromCopilot?: boolean;
} | {
type: "clear_pending_triggers";
} | { } | {
type: "select_agent"; type: "select_agent";
name: string; name: string;
@ -618,30 +587,6 @@ function reducer(state: State, action: Action): State {
draft.chatKey++; draft.chatKey++;
break; 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": case "delete_agent":
// Remove the agent // Remove the agent
draft.workflow.agents = draft.workflow.agents.filter( draft.workflow.agents = draft.workflow.agents.filter(
@ -1370,38 +1315,6 @@ export function WorkflowEditor({
} }
}, [state.present.agentInstructionsChanged, markAgentInstructionsChanged]); }, [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) { function handleSelectAgent(name: string) {
dispatch({ type: "select_agent", name }); dispatch({ type: "select_agent", name });
} }

View file

@ -268,7 +268,7 @@ function CopilotStatusBar({
// Show real button when ready // Show real button when ready
return ( return (
<button <button
onClick={handleApplyAll} onClick={() => { void handleApplyAll?.(); }}
disabled={allApplied} disabled={allApplied}
className={`flex items-center gap-2 px-3 py-1 rounded-full font-medium text-xs transition-colors duration-200 className={`flex items-center gap-2 px-3 py-1 rounded-full font-medium text-xs transition-colors duration-200
${ ${