PluginProbe
Extendify / 3.2.3
Extendify v3.2.3
3.2.3 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 All 129 releases
← All changes | src/Agent/Agent.jsx +144 -45 3.2.0 → 3.2.3 View file →
@@ -12,9 +12,16 @@
12 12 useCanvasOpen,
13 13 } from '@agent/components/Canvas';
14 14 import { ChatInput } from '@agent/components/ChatInput';
15 15 import { ChatMessages } from '@agent/components/ChatMessages';
16 +import { StatusIndicator } from '@agent/components/messages/StatusIndicator';
16 17 import { UsageMessage } from '@agent/components/messages/UsageMessage';
18 +import { useFollowUpHistory } from '@agent/follow-ups/history';
19 +import {
20 + cardMessageIndex,
21 + pickNextCard,
22 + toolCallsThisRun,
23 +} from '@agent/follow-ups/pick-next';
17 24 import { useLockPost } from '@agent/hooks/useLockPost';
18 25 import { isAbilityTool, isAbilityWorkflow } from '@agent/lib/abilities';
19 26 import {
20 27 getClientToolFallbackReply,
@@ -20,17 +27,16 @@
20 27 getClientToolFallbackReply,
21 28 getClientTools,
22 29 } from '@agent/lib/client-tools';
23 30 import { localPickWorkflow } from '@agent/lib/local-pick';
24 -import { getRedirectUrl } from '@agent/lib/redirects';
25 31 import { doReload } from '@agent/lib/reload';
26 32 import { useCanvasStore } from '@agent/state/canvas';
27 33 import { useChatStore } from '@agent/state/chat';
28 34 import { useGlobalStore } from '@agent/state/global';
29 35 import { useStatusStore } from '@agent/state/status';
30 -import { useSuggestionsStore } from '@agent/state/suggestions';
31 36 import { useWorkflowStore } from '@agent/state/workflows';
32 37 import { hasRunComponent } from '@agent/workflows/abilities/components/run';
38 +import startOnboardingWorkflow from '@agent/workflows/misc/start-onboarding';
33 39 import { useQuickEditStore } from '@quick-edit/state/store';
34 40 import { digest } from '@shared/api/digest';
35 41 import {
36 42 useCallback,
@@ -65,8 +71,18 @@
65 71 await new Promise((resolve) => setTimeout(resolve, ms));
66 72 return signal.aborted;
67 73 };
68 74
75 +const nextSteps = (workflowId, status, messages) => {
76 + if (status === 'completed') {
77 + useFollowUpHistory.getState().markDone(workflowId);
78 + }
79 + const card = pickNextCard({ workflowId, status, messages });
80 + return card
81 + ? { followUp: card.id, followUpData: card.data }
82 + : { followUp: null };
83 +};
84 +
69 85 export const Agent = () => {
70 86 const { addMessage, updateMessage, popMessage, messages } = useChatStore();
71 87 const { pushStatus, clearStatuses, leavingPage } = useStatusStore();
72 88 const {
@@ -99,9 +115,8 @@
99 115 const canvasOpen = useCanvasOpen();
100 116 const canvasAssist = useCanvasAssist();
101 117 const canvasNoticeShown = useRef(false);
102 118 const chatAvailable = useMemo(() => isChatAvailable(), [isChatAvailable]);
103 - const { addSuggestions, getSuggestions } = useSuggestionsStore();
104 119 // Options render only while their message is last; a reply dismisses them.
105 120 const lastMessage = messages.at(-1);
106 121 const qaSuggestions =
107 122 lastMessage?.type === 'message' &&
@@ -109,8 +124,18 @@
109 124 Array.isArray(lastMessage.details?.qaSuggestions)
110 125 ? lastMessage.details.qaSuggestions
111 126 : null;
112 127
128 + useEffect(() => {
129 + const { messages, updateMessage } = useChatStore.getState();
130 + const index = cardMessageIndex(messages);
131 + const { id, details } = messages[index] ?? {};
132 + if (!id || details.followUp !== undefined || details.followUpClosed) return;
133 + const { workflowId, status } = details;
134 + const steps = nextSteps(workflowId, status, messages.slice(0, index));
135 + if (steps.followUp) updateMessage(id, steps);
136 + }, []);
137 +
113 138 // Without this the input stays disabled for as long as the canvas is open.
114 139 useEffect(() => {
115 140 if (canvasOpen && canvasAssist) setCanType(true);
116 141 // A workflow reached by example carries no sessionId to key this on.
@@ -126,9 +151,14 @@
126 151 agentBlock && setBlock(null);
127 152 // A class or pin left up freezes Quick Edit hover site-wide.
128 153 document
129 154 .querySelector('.wp-site-blocks')
130 - ?.classList.remove('extendify-agent-working', 'extendify-agent-busy');
155 + ?.classList.remove(
156 + 'extendify-agent-working',
157 + 'extendify-agent-busy',
158 + 'extendify-agent-waiting',
159 + 'extendify-agent-task',
160 + );
131 161 setCommittedSelection(null);
132 162 clearStatuses();
133 163 window.dispatchEvent(new Event('extendify-agent:remove-block-highlight'));
134 164 }, [setBlock, clearStatuses]);
@@ -271,18 +301,34 @@
271 301 setCanType(true);
272 302 }, [addMessage, pushStatus, whenFinishedToolProps, workflow]);
273 303
274 304 const handleSubmit = useCallback(
275 - async (message) => {
276 - // Suggestions reach the agent without the textarea; disabling it isn't enough.
305 + async (message, { hidden = false, step } = {}) => {
306 + // chat-submit events skip the textarea; disabling it isn't enough.
277 307 if (useQuickEditStore.getState().selected) return;
308 + if (whenFinishedToolProps?.processing) return;
278 309 setWaitingOnToolOrUser(false);
279 310 agentWorking.current = false;
280 - addMessage('message', { role: 'user', content: message });
311 + if (step) addMessage('tool', { ...step, inputs: {}, fromCard: true });
312 + addMessage('message', { role: 'user', content: message, hidden });
281 313
282 314 // Without this a typed message would drop the workflow and close the canvas.
283 315 if (canvasOpen && canvasAssist) return handleCanvasMessage();
284 316
317 + // A staged tool the user walked away from still owes its receipt.
318 + if (workflow?.id && whenFinishedToolProps?.id) {
319 + const { answerId, whenFinishedTool } =
320 + whenFinishedToolProps.agentResponse || {};
321 + addMessage('workflow', {
322 + status: 'canceled',
323 + label: whenFinishedTool?.labels?.cancel,
324 + agent: workflow.agent,
325 + workflowId: workflow.id,
326 + answerId,
327 + followUp: null,
328 + });
329 + }
330 +
285 331 // Let some phrases auto load workflows
286 332 const bypass = getWorkflowByExample(message);
287 333 if (bypass?.example?.agentResponse) {
288 334 // whenFinishedTool → inline input UI; none → ask for a typed reply.
@@ -387,9 +433,10 @@
387 433 useEffect(() => {
388 434 // Allow external messages to trigger the agent
389 435 const handleMessage = ({ detail }) => {
390 436 if (!detail?.message) return;
391 - handleSubmit(detail.message);
437 + const { message, hidden, step } = detail;
438 + handleSubmit(message, { hidden, step });
392 439 };
393 440 // Allow external code to clear the block and workflow
394 441 const handleCleanup = () => {
395 442 // cleanup() resets canType and agentWorking, so read them before it runs.
@@ -407,9 +454,9 @@
407 454 addMessage('workflow', {
408 455 status: 'canceled',
409 456 agent: workflow?.agent,
410 457 workflowId: workflow?.id,
411 - suggestions: getSuggestions(),
458 + ...nextSteps(workflow?.id, 'canceled'),
412 459 });
413 460 return;
414 461 };
415 462 window.addEventListener('extendify-agent:cancel-workflow', handleCleanup);
@@ -427,26 +474,52 @@
427 474 setWorkflow,
428 475 addMessage,
429 476 workflow,
430 477 qaSuggestions,
431 - getSuggestions,
432 478 canType,
433 479 ]);
434 480
481 + // Dispatching before chat-submit has a listener drops the workflow.
482 + useEffect(() => {
483 + if (!startOnboardingWorkflow.available()) return;
484 + window.dispatchEvent(
485 + new CustomEvent('extendify-agent:chat-submit', {
486 + detail: { message: startOnboardingWorkflow.example.text, hidden: true },
487 + }),
488 + );
489 + }, []);
490 +
491 + // ChatMessages hides a confirm restored by a reload; left open, the workflow would sit unfinishable.
492 + useEffect(() => {
493 + const { getWorkflow, reloadedToolProps } = useWorkflowStore.getState();
494 + const { whenFinished, needsRedirect } = getWorkflow() ?? {};
495 + if (!reloadedToolProps?.id || !whenFinished?.component) return;
496 + if (whenFinished.canvas || needsRedirect?.()) return;
497 + window.dispatchEvent(new Event('extendify-agent:cancel-workflow'));
498 + }, []);
499 +
435 500 // Handle whenFinished component confirm/cancel
436 501 useEffect(() => {
437 502 const handleConfirm = async ({ detail }) => {
438 503 if (toolWorking.current) return;
439 - setWhenFinishedToolProps(null);
440 504 toolWorking.current = true;
441 505 const { data, whenFinishedToolProps, shouldRefreshPage, redirectUrl } =
442 506 detail ?? {};
443 - const { whenFinishedTool, answerId, redirectTo } =
507 + // Staged until the tool returns, so the chat shows it saving.
508 + setWhenFinishedToolProps(
509 + whenFinishedToolProps
510 + ? { ...whenFinishedToolProps, processing: true }
511 + : null,
512 + );
513 + const { whenFinishedTool, answerId } =
444 514 whenFinishedToolProps?.agentResponse || {};
445 515 const { id, labels } = whenFinishedTool || {};
446 516 // Staged unanswered so its own component can show the run in progress.
447 517 const runMessageId = id ? addMessage('tool', { id, inputs: data }) : null;
448 - if (!hasRunComponent(id)) pushStatus('workflow-tool-processing');
518 + // In the chat, the saving state shows this label instead.
519 + if (!hasRunComponent(id) && workflow?.whenFinished?.canvas) {
520 + pushStatus('workflow-tool-processing', labels?.started);
521 + }
449 522 // Not all workflows have a tool at the end (e.g. tours)
450 523 const toolResponse = await callTool?.({ tool: id, inputs: data }).catch(
451 524 (error) => {
452 525 const { sessionId } = workflow || {};
@@ -462,8 +535,9 @@
462 535 return { error: { message: error?.message, code: error?.code } };
463 536 },
464 537 );
465 538 toolWorking.current = false;
539 + setWhenFinishedToolProps(null);
466 540 if (runMessageId) updateMessage(runMessageId, { result: toolResponse });
467 541 if (toolResponse?.refused) {
468 542 await new Promise((resolve) => setTimeout(resolve, 1000));
469 543 const refusalMessages = {
@@ -496,9 +570,13 @@
496 570 setLoop((prev) => prev + 1);
497 571 return;
498 572 }
499 573
500 - addSuggestions(whenFinishedToolProps.agentResponse?.recommendations);
574 + // Reloading here unmounts a run component still watching its ability finish.
575 + const refreshForAbility =
576 + isAbilityTool(id) && shouldRefreshPage !== false;
577 + const willReload = redirectUrl || shouldRefreshPage || refreshForAbility;
578 + // A card shown now would flash just before the reload.
501 579 addMessage('workflow', {
502 580 status: 'completed',
503 581 label: labels?.confirm,
504 582 agent: workflow.agent,
@@ -503,9 +581,9 @@
503 581 label: labels?.confirm,
504 582 agent: workflow.agent,
505 583 workflowId: workflow.id,
506 584 answerId,
507 - suggestions: getSuggestions(),
585 + ...(willReload ? {} : nextSteps(workflow.id, 'completed')),
508 586 });
509 587 // A chat message persists, so the next turn's model knows the save was partial.
510 588 const refusedCount = toolResponse?.refusedOperations?.length;
511 589 if (refusedCount) {
@@ -525,13 +603,9 @@
525 603 }
526 604 setWorkflow(null);
527 605 useCanvasStore.getState().endSession();
528 606
529 - const url = getRedirectUrl(redirectTo, whenFinishedToolProps?.inputs);
530 - const refreshForAbility = isAbilityTool(id);
531 - if (url || redirectUrl || shouldRefreshPage || refreshForAbility) {
532 - return doReload(url || redirectUrl);
533 - }
607 + if (willReload) return doReload(redirectUrl);
534 608 cleanup();
535 609 };
536 610 const handleCancel = ({ detail }) => {
537 611 if (toolWorking.current) return;
@@ -544,9 +618,9 @@
544 618 label: whenFinishedTool?.labels?.cancel,
545 619 agent: workflow.agent,
546 620 workflowId: workflow.id,
547 621 answerId,
548 - suggestions: getSuggestions(),
622 + ...nextSteps(workflow.id, 'canceled'),
549 623 });
550 624 setWorkflow(null);
551 625 useCanvasStore.getState().endSession();
552 626 cleanup();
@@ -571,18 +645,9 @@
571 645 handleCancel,
572 646 );
573 647 window.removeEventListener('extendify-agent:workflow-retry', handleRetry);
574 648 };
575 - }, [
576 - addMessage,
577 - pushStatus,
578 - popMessage,
579 - cleanup,
580 - setWorkflow,
581 - workflow,
582 - getSuggestions,
583 - addSuggestions,
584 - ]);
649 + }, [addMessage, pushStatus, popMessage, cleanup, setWorkflow, workflow]);
585 650
586 651 useEffect(() => {
587 652 const handleClose = () => setOpen(false);
588 653 const handleOpen = () => setOpen(true);
@@ -602,9 +667,26 @@
602 667 if (open) return;
603 668 if (block) setBlock(null);
604 669 }, [open, block, setBlock]);
605 670
671 + // A pick answers select-block's "which one?"; making the user also type stalls it.
672 + const previousBlock = useRef(block);
606 673 useEffect(() => {
674 + const picked = block && !previousBlock.current;
675 + previousBlock.current = block;
676 + if (!picked || workflow?.id !== 'select-block') return;
677 + if (!waitingOnToolOrUser || !canType || whenFinishedToolProps?.id) return;
678 + handleSubmit('This is the block I mean.', { hidden: true });
679 + }, [
680 + block,
681 + workflow?.id,
682 + waitingOnToolOrUser,
683 + canType,
684 + whenFinishedToolProps?.id,
685 + handleSubmit,
686 + ]);
687 +
688 + useEffect(() => {
607 689 if (waitingOnToolOrUser || !open || !workflow?.id) return;
608 690 // Some workflows require they dont change pages
609 691 const theyMoved = workflow?.startingPage !== window.location.href;
610 692 // Requires a block to be selected
@@ -615,9 +697,9 @@
615 697 addMessage('workflow', {
616 698 status: 'canceled',
617 699 agent: workflow.agent,
618 700 workflowId: workflow.id,
619 - suggestions: getSuggestions(),
701 + ...nextSteps(workflow.id, 'canceled'),
620 702 });
621 703 setWorkflow(null);
622 704 cleanup();
623 705 return;
@@ -714,9 +796,8 @@
714 796 status: 'completed',
715 797 inputs,
716 798 workflowId: workflow.id,
717 799 });
718 - addSuggestions(agentResponse.recommendations);
719 800 setWorkflow(null);
720 801 addMessage('workflow', {
721 802 status: 'completed',
722 803 agent: workflow.agent,
@@ -721,9 +802,9 @@
721 802 status: 'completed',
722 803 agent: workflow.agent,
723 804 workflowId: workflow.id,
724 805 answerId,
725 - suggestions: getSuggestions(),
806 + ...nextSteps(workflow.id, 'completed'),
726 807 });
727 808 cleanup();
728 809 }
729 810 return;
@@ -729,19 +810,21 @@
729 810 return;
730 811 }
731 812 // If we're done, it means the AI has the answer
732 813 if (agentResponse.status !== 'in-progress') {
733 - const { recommendations, status } = agentResponse;
734 - const isCompleted = status === 'completed';
735 - if (recommendations) addSuggestions(recommendations);
814 + const status =
815 + agentResponse.status === 'completed' ? 'completed' : 'canceled';
816 + // A reply that changed nothing isn't a finished task to follow up.
817 + const didWork =
818 + toolCallsThisRun(useChatStore.getState().messages).length > 0;
736 819 setWorkflow(null);
737 820 cleanup();
738 821 addMessage('workflow', {
739 - status: isCompleted ? 'completed' : 'canceled',
822 + status,
740 823 agent: workflow.agent,
741 824 workflowId: workflow.id,
742 825 answerId,
743 - suggestions: getSuggestions(),
826 + ...(didWork ? nextSteps(workflow.id, status) : { followUp: null }),
744 827 });
745 828 return;
746 829 }
747 830 if (sessionId && sessionId !== workflow.sessionId) {
@@ -761,9 +844,10 @@
761 844 setWaitingOnToolOrUser(true);
762 845 setCanType(false);
763 846 return;
764 847 }
765 - pushStatus('tool-started', labels?.started);
848 + // A reply sent with the tool clears the status, blanking the line.
849 + pushStatus('agent-working');
766 850 const toolData = await Promise.all([
767 851 callTool({ tool: id, inputs }),
768 852 new Promise((resolve) => setTimeout(resolve, 3000)),
769 853 ])
@@ -794,8 +878,9 @@
794 878 id,
795 879 inputs,
796 880 result: toolData,
797 881 label: labels?.confirm,
882 + started: labels?.started,
798 883 });
799 884 setWaitingOnToolOrUser(false);
800 885 agentWorking.current = false;
801 886 setLoop((prev) => prev + 1); // Trigger next loop
@@ -841,10 +926,8 @@
841 926 setWhenFinishedToolProps,
842 927 block,
843 928 setBlock,
844 929 findAgent,
845 - addSuggestions,
846 - getSuggestions,
847 930 ]);
848 931
849 932 useEffect(() => {
850 933 if (!canType) return;
@@ -850,9 +933,15 @@
850 933 if (!canType) return;
851 934 document.querySelector('#extendify-agent-chat-textarea')?.focus();
852 935 }, [canType]);
853 936
854 - const busy = !canType || !chatAvailable || workflow?.id;
937 + // A task waiting on a reply leaves the page pickable so the user can answer by pointing.
938 + const waiting =
939 + canType &&
940 + chatAvailable &&
941 + Boolean(workflow?.id) &&
942 + !whenFinishedToolProps?.id;
943 + const busy = !canType || !chatAvailable || (workflow?.id && !waiting);
855 944 // `busy` is true at rest; 429 is blocked, not working.
856 945 const working = !canType && chatAvailable;
857 946
858 947 return (
@@ -857,9 +946,14 @@
857 946
858 947 return (
859 948 <>
860 949 <Canvas />
861 - <Chat busy={busy} working={working}>
950 + <Chat
951 + busy={busy}
952 + working={working}
953 + waiting={waiting}
954 + taskActive={Boolean(workflow?.id)}
955 + >
862 956 <div className="relative z-50 flex h-full flex-col justify-between overflow-auto">
863 957 <ChatMessages
864 958 redirectComponent={
865 959 workflow?.needsRedirect?.() ? workflow.redirectComponent : null
@@ -874,11 +968,16 @@
874 968 }}
875 969 />
876 970 </div>
877 971 <div className="p-4 pb-2 pt-0">
972 + <StatusIndicator />
878 973 <ChatInput
879 974 disabled={
880 - !canType || !chatAvailable || !!qaSuggestions || leavingPage
975 + !canType ||
976 + !chatAvailable ||
977 + !!qaSuggestions ||
978 + leavingPage ||
979 + whenFinishedToolProps?.processing
881 980 }
882 981 handleSubmit={handleSubmit}
883 982 />
884 983 </div>