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>
)}
<Messages
projectId={projectId}
messages={messages}
streamingResponse={streamingResponse}
loadingResponse={loadingResponse}

View file

@ -171,14 +171,16 @@ function AssistantMessage({
dispatch,
messageIndex,
loading,
onStatusBarChange
onStatusBarChange,
projectId,
}: {
content: z.infer<typeof CopilotAssistantMessage>['content'],
workflow: z.infer<typeof Workflow>,
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<Set<number>>(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<boolean> => {
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<typeof CopilotMessage>[];
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) {

View file

@ -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<typeof Message>[];
};
}>;
}
interface State {
@ -153,28 +144,6 @@ export type Action = {
pipeline: Partial<z.infer<typeof WorkflowPipeline>>;
defaultModel?: string;
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";
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 });
}

View file

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