mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +02:00
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:
parent
24f930fa86
commit
651a998fbb
4 changed files with 106 additions and 151 deletions
|
|
@ -255,6 +255,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
|
|||
</div>
|
||||
)}
|
||||
<Messages
|
||||
projectId={projectId}
|
||||
messages={messages}
|
||||
streamingResponse={streamingResponse}
|
||||
loadingResponse={loadingResponse}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
${
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue