PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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 0.6.0 All 127 releases
← All changes | src/Agent/Agent.jsx +48 -23 3.1.63.2.1 View file →
@@ -29,8 +29,9 @@
29 29 import { useStatusStore } from '@agent/state/status';
30 30 import { useSuggestionsStore } from '@agent/state/suggestions';
31 31 import { useWorkflowStore } from '@agent/state/workflows';
32 32 import { hasRunComponent } from '@agent/workflows/abilities/components/run';
33 +import startOnboardingWorkflow from '@agent/workflows/misc/start-onboarding';
33 34 import { useQuickEditStore } from '@quick-edit/state/store';
34 35 import { digest } from '@shared/api/digest';
35 36 import {
36 37 useCallback,
@@ -58,8 +59,15 @@
58 59 ]);
59 60 return result;
60 61 };
61 62
63 +// cleanup() swaps in a fresh controller; a signal read after the wait is never aborted.
64 +const canceledDuring = async (ms) => {
65 + const { signal } = controller;
66 + await new Promise((resolve) => setTimeout(resolve, ms));
67 + return signal.aborted;
68 +};
69 +
62 70 export const Agent = () => {
63 71 const { addMessage, updateMessage, popMessage, messages } = useChatStore();
64 72 const { pushStatus, clearStatuses, leavingPage } = useStatusStore();
65 73 const {
@@ -143,13 +151,14 @@
143 151
144 152 const findAgent = useCallback(
145 153 async (options = {}) => {
146 154 pushStatus('calling-agent');
155 + const { signal } = controller;
147 156 const response = await withMinDuration(
148 157 pickWorkflow({
149 158 // A mid-turn drop clears the block after this closure was made.
150 159 workflows: getAvailableWorkflows().map((w) => w.id),
151 - options: { signal: controller.signal, ...options },
160 + options: { signal, ...options },
152 161 }).catch(async (error) => {
153 162 devmode && console.error(error);
154 163 if (error?.response?.status === 429) {
155 164 updateRetryAfter(error?.response?.headers?.get('Retry-After'));
@@ -157,15 +166,9 @@
157 166 pushStatus('credits-exhausted');
158 167 return;
159 168 }
160 169 setCanType(true);
161 - if (error === 'Workflow aborted') {
162 - addMessage('workflow', {
163 - status: 'canceled',
164 - suggestions: getSuggestions(),
165 - });
166 - return;
167 - }
170 + if (error === 'Workflow aborted') return;
168 171
169 172 await new Promise((resolve) => setTimeout(resolve, 1000));
170 173 addMessage('message', {
171 174 role: 'assistant',
@@ -179,9 +182,9 @@
179 182 return;
180 183 }),
181 184 500,
182 185 );
183 - if (!response) return;
186 + if (!response || signal.aborted) return;
184 187
185 188 const { workflow: wf, reply } = response;
186 189 if (wf?.id) setWorkflow(wf);
187 190 if (reply) {
@@ -195,9 +198,8 @@
195 198 pushStatus,
196 199 updateRetryAfter,
197 200 setWorkflow,
198 201 getAvailableWorkflows,
199 - getSuggestions,
200 202 ],
201 203 );
202 204
203 205 // The backend caps tool runs; nothing here bounds the loop.
@@ -270,18 +272,32 @@
270 272 setCanType(true);
271 273 }, [addMessage, pushStatus, whenFinishedToolProps, workflow]);
272 274
273 275 const handleSubmit = useCallback(
274 - async (message) => {
276 + async (message, { hidden = false } = {}) => {
275 277 // Suggestions reach the agent without the textarea; disabling it isn't enough.
276 278 if (useQuickEditStore.getState().selected) return;
277 279 setWaitingOnToolOrUser(false);
278 280 agentWorking.current = false;
279 - addMessage('message', { role: 'user', content: message });
281 + addMessage('message', { role: 'user', content: message, hidden });
280 282
281 283 // Without this a typed message would drop the workflow and close the canvas.
282 284 if (canvasOpen && canvasAssist) return handleCanvasMessage();
283 285
286 + // A staged tool the user walked away from still owes its receipt.
287 + if (workflow?.id && whenFinishedToolProps?.id) {
288 + const { answerId, whenFinishedTool } =
289 + whenFinishedToolProps.agentResponse || {};
290 + addMessage('workflow', {
291 + status: 'canceled',
292 + label: whenFinishedTool?.labels?.cancel,
293 + agent: workflow.agent,
294 + workflowId: workflow.id,
295 + answerId,
296 + suggestions: getSuggestions(),
297 + });
298 + }
299 +
284 300 // Let some phrases auto load workflows
285 301 const bypass = getWorkflowByExample(message);
286 302 if (bypass?.example?.agentResponse) {
287 303 // whenFinishedTool → inline input UI; none → ask for a typed reply.
@@ -309,9 +325,9 @@
309 325
310 326 // Skip the network find-agent when the staged block makes the edit certain.
311 327 const localPick = localPickWorkflow({ block });
312 328 if (localPick) {
313 - await new Promise((resolve) => setTimeout(resolve, 500));
329 + if (await canceledDuring(500)) return;
314 330 setWorkflow(localPick);
315 331 return;
316 332 }
317 333
@@ -329,8 +345,9 @@
329 345 setWorkflow,
330 346 workflow,
331 347 workflowData,
332 348 getAvailableWorkflows,
349 + getSuggestions,
333 350 ],
334 351 );
335 352
336 353 // Used to inject a workflow final state
@@ -340,9 +357,9 @@
340 357 if (!agentResponse) return;
341 358 setWorkflow(workflow);
342 359 setCanType(false);
343 360 agentWorking.current = true;
344 - await new Promise((resolve) => setTimeout(resolve, 750));
361 + if (await canceledDuring(750)) return;
345 362 addMessage('message', {
346 363 role: 'assistant',
347 364 content: agentResponse.reply,
348 365 });
@@ -366,9 +383,9 @@
366 383 // Without this the loop calls the backend before the user has typed.
367 384 setWaitingOnToolOrUser(true);
368 385 setCanType(false);
369 386 agentWorking.current = true;
370 - await new Promise((resolve) => setTimeout(resolve, 750));
387 + if (await canceledDuring(750)) return;
371 388 addMessage('message', {
372 389 role: 'assistant',
373 390 content: agentResponse.reply,
374 391 // A workflow example can carry its own suggestions; none still asks.
@@ -386,12 +403,14 @@
386 403 useEffect(() => {
387 404 // Allow external messages to trigger the agent
388 405 const handleMessage = ({ detail }) => {
389 406 if (!detail?.message) return;
390 - handleSubmit(detail.message);
407 + handleSubmit(detail.message, { hidden: detail.hidden });
391 408 };
392 409 // Allow external code to clear the block and workflow
393 410 const handleCleanup = () => {
411 + // cleanup() resets canType and agentWorking, so read them before it runs.
412 + const interrupted = agentWorking.current || !canType;
394 413 controller.abort('Workflow aborted');
395 414 cleanup();
396 415 // Deferred a frame: the input stays disabled until the cancel lands.
397 416 requestAnimationFrame(() =>
@@ -398,9 +417,9 @@
398 417 document.querySelector('#extendify-agent-chat-textarea')?.focus(),
399 418 );
400 419
401 420 // An options panel can outlive its workflow; cancel must still clear it.
402 - if (!workflow?.id && !qaSuggestions) return;
421 + if (!workflow?.id && !qaSuggestions && !interrupted) return;
403 422 setWorkflow(null);
404 423 addMessage('workflow', {
405 424 status: 'canceled',
406 425 agent: workflow?.agent,
@@ -425,10 +444,21 @@
425 444 addMessage,
426 445 workflow,
427 446 qaSuggestions,
428 447 getSuggestions,
448 + canType,
429 449 ]);
430 450
451 + // Dispatching before chat-submit has a listener drops the workflow.
452 + useEffect(() => {
453 + if (!startOnboardingWorkflow.available()) return;
454 + window.dispatchEvent(
455 + new CustomEvent('extendify-agent:chat-submit', {
456 + detail: { message: startOnboardingWorkflow.example.text, hidden: true },
457 + }),
458 + );
459 + }, []);
460 +
431 461 // Handle whenFinished component confirm/cancel
432 462 useEffect(() => {
433 463 const handleConfirm = async ({ detail }) => {
434 464 if (toolWorking.current) return;
@@ -467,13 +497,8 @@
467 497 'no-op': __(
468 498 "That edit came back unchanged, which wasn't expected. Please try rephrasing what you'd like to change.",
469 499 'extendify-local',
470 500 ),
471 - // translators: Shown when the user asked the AI agent to edit a block that lives in a site template (e.g. the header or footer), which the agent cannot save yet.
472 - 'template-part': __(
473 - "That block is part of your site's template, like the header or footer, and I can't make changes there yet.",
474 - 'extendify-local',
475 - ),
476 501 };
477 502 addMessage('message', {
478 503 role: 'assistant',
479 504 content:
@@ -693,9 +718,9 @@
693 718 // translators: Shown while the AI agent deselects a block that can't serve the request.
694 719 __('Removing selected block', 'extendify-local'),
695 720 );
696 721 // findAgent pushes its own status right away; let this one read first.
697 - await new Promise((resolve) => setTimeout(resolve, 2500));
722 + if (await canceledDuring(2500)) return;
698 723 agentWorking.current = false;
699 724 await findAgent();
700 725 return;
701 726 }