PluginProbe
Extendify / trunk
Extendify vtrunk
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.6 → trunk 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 }