PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 All 128 releases
extendify / src / Agent / state / workflows.js

workflows.js in Extendify 3.2.2, at src/Agent/state/workflows.js

137 lines 4.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { isAbilityWorkflow } from '@agent/lib/abilities';
2 import { AbilityRun } from '@agent/workflows/abilities/components/run';
3 import { abilityWorkflows, workflows } from '@agent/workflows/workflows';
4 import { useQuickEditStore } from '@quick-edit/state/store';
5 import { deepMerge } from '@shared/lib/utils';
6 import { create } from 'zustand';
7 import { devtools, persist } from 'zustand/middleware';
8
9 // Used to check case-insensitive matches for workflow examples
10 const collator = new Intl.Collator(undefined, {
11 sensitivity: 'base',
12 usage: 'search',
13 });
14
15 const state = (set, get) => ({
16 workflow: null,
17 // Data for the tool component that shows up at the end of a workflow
18 whenFinishedToolProps: null,
19 // A staged tool restored from storage; its confirm never renders.
20 reloadedToolProps: null,
21 getWorkflow: () => {
22 const curr = get().workflow;
23 // Workflows may define a "parent" workflow via templateId
24 const currId = curr?.templateId || curr?.id;
25 const wf = workflows.find(({ id }) => id === currId);
26 if (!wf?.id) {
27 if (!curr) return null;
28 if (!isAbilityWorkflow(curr.id)) return curr;
29 // The backend never sends whenFinished, so this supplies the gate.
30 const ability = deepMerge(
31 { whenFinished: { component: AbilityRun } },
32 abilityWorkflows[curr.id] ?? {},
33 );
34 return { ...deepMerge(curr, ability), id: curr.id };
35 }
36 return { ...deepMerge(curr, wf || {}), id: curr?.id };
37 },
38 getWorkflowByExample: (example) => {
39 const wf = workflows.find(
40 ({ example: ex }) => collator.compare(ex?.text, example) === 0,
41 );
42 if (!wf?.id) return null;
43 if (!wf.available()) return null;
44 return wf;
45 },
46 // Gets the workflows available to the user
47 // TODO: maybe we need to have a way to include a
48 // workflow regardless of the block being active?
49 getAvailableWorkflows: () => {
50 const wfs = workflows.filter(({ available }) => available());
51 // If a block is set, only include those with 'block'
52 const blockWorkflows = wfs.filter(({ requires }) =>
53 requires?.includes('block'),
54 );
55 if (useQuickEditStore.getState().agentBlock) return blockWorkflows;
56 // otherwise remove all of the above
57 return wfs.filter(({ id }) => !blockWorkflows.some((w) => w.id === id));
58 },
59 getWorkflowsByFeature: ({ requires } = {}) => {
60 if (!requires) return workflows.filter(({ available }) => available());
61 // e.g. requires: ['block']
62 return workflows.filter(
63 ({ available, requires: workflowRequires }) =>
64 available() &&
65 (!requires || workflowRequires?.some((s) => requires.includes(s))),
66 );
67 },
68 workflowData: null,
69 mergeWorkflowData: (data) => {
70 set((state) => {
71 if (!state.workflowData) return { workflowData: data };
72 return {
73 workflowData: { ...state.workflowData, ...data },
74 };
75 });
76 },
77 setWorkflow: (workflow) => {
78 const agentBlockCode = useQuickEditStore.getState().agentBlockCode;
79 const started = workflow
80 ? { ...workflow, startingPage: window.location.href }
81 : null;
82 set({
83 workflow: started,
84 // If a block is selected, add it to the workflow data
85 // previousContent is named this way for legacy reasons
86 workflowData: agentBlockCode ? { previousContent: agentBlockCode } : null,
87 whenFinishedToolProps: null,
88 });
89 if (!started) return;
90 const workflowStarted = get().getWorkflow();
91 window.dispatchEvent(
92 new CustomEvent('extendify-workflow::started', {
93 detail: workflowStarted,
94 }),
95 );
96 try {
97 workflowStarted?.onStarted?.(workflowStarted);
98 } catch {
99 // A workflow that can't read the page still has to start.
100 }
101 },
102 setWhenFinishedToolProps: (whenFinishedToolProps) =>
103 set({ whenFinishedToolProps }),
104 // Declared up front it would hide the workflow while nothing is staged.
105 // Not setWorkflow: that resets workflowData and drops the run's data.
106 requireBlock: () =>
107 set((state) =>
108 state.workflow
109 ? { workflow: { ...state.workflow, requires: ['block'] } }
110 : {},
111 ),
112 });
113
114 export const useWorkflowStore = create()(
115 persist(devtools(state, { name: 'Extendify Agent Workflows' }), {
116 name: `extendify-agent-workflows-${window.extSharedData.siteId}`,
117 merge: (persistedState, currentState) => {
118 const merged = { ...currentState, ...persistedState };
119 // A reload can't carry the staged block a block-patching workflow
120 // depends on, so a rehydrated-open one is always stale. A canvas
121 // session is stale too: its field values never persist.
122 const id = merged.workflow?.templateId || merged.workflow?.id;
123 const registered = workflows.find((wf) => wf.id === id);
124 if (id === 'block-patching' || registered?.whenFinished?.canvas) {
125 return {
126 ...merged,
127 workflow: null,
128 whenFinishedToolProps: null,
129 workflowData: null,
130 };
131 }
132 return { ...merged, reloadedToolProps: merged.whenFinishedToolProps };
133 },
134 partialize: ({ reloadedToolProps, ...persisted }) => persisted,
135 }),
136 );
137