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
← All changes | src/Agent/Agent.jsx +116 -42 3.2.1 → 3.2.2 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,
@@ -26,9 +33,8 @@
26 33 import { useCanvasStore } from '@agent/state/canvas';
27 34 import { useChatStore } from '@agent/state/chat';
28 35 import { useGlobalStore } from '@agent/state/global';
29 36 import { useStatusStore } from '@agent/state/status';
30 -import { useSuggestionsStore } from '@agent/state/suggestions';
31 37 import { useWorkflowStore } from '@agent/state/workflows';
32 38 import { hasRunComponent } from '@agent/workflows/abilities/components/run';
33 39 import startOnboardingWorkflow from '@agent/workflows/misc/start-onboarding';
34 40 import { useQuickEditStore } from '@quick-edit/state/store';
@@ -66,8 +72,18 @@
66 72 await new Promise((resolve) => setTimeout(resolve, ms));
67 73 return signal.aborted;
68 74 };
69 75
76 +const nextSteps = (workflowId, status, messages) => {
77 + if (status === 'completed') {
78 + useFollowUpHistory.getState().markDone(workflowId);
79 + }
80 + const card = pickNextCard({ workflowId, status, messages });
81 + return card
82 + ? { followUp: card.id, followUpData: card.data }
83 + : { followUp: null };
84 +};
85 +
70 86 export const Agent = () => {
71 87 const { addMessage, updateMessage, popMessage, messages } = useChatStore();
72 88 const { pushStatus, clearStatuses, leavingPage } = useStatusStore();
73 89 const {
@@ -100,9 +116,8 @@
100 116 const canvasOpen = useCanvasOpen();
101 117 const canvasAssist = useCanvasAssist();
102 118 const canvasNoticeShown = useRef(false);
103 119 const chatAvailable = useMemo(() => isChatAvailable(), [isChatAvailable]);
104 - const { addSuggestions, getSuggestions } = useSuggestionsStore();
105 120 // Options render only while their message is last; a reply dismisses them.
106 121 const lastMessage = messages.at(-1);
107 122 const qaSuggestions =
108 123 lastMessage?.type === 'message' &&
@@ -110,8 +125,18 @@
110 125 Array.isArray(lastMessage.details?.qaSuggestions)
111 126 ? lastMessage.details.qaSuggestions
112 127 : null;
113 128
129 + useEffect(() => {
130 + const { messages, updateMessage } = useChatStore.getState();
131 + const index = cardMessageIndex(messages);
132 + const { id, details } = messages[index] ?? {};
133 + if (!id || details.followUp !== undefined || details.followUpClosed) return;
134 + const { workflowId, status } = details;
135 + const steps = nextSteps(workflowId, status, messages.slice(0, index));
136 + if (steps.followUp) updateMessage(id, steps);
137 + }, []);
138 +
114 139 // Without this the input stays disabled for as long as the canvas is open.
115 140 useEffect(() => {
116 141 if (canvasOpen && canvasAssist) setCanType(true);
117 142 // A workflow reached by example carries no sessionId to key this on.
@@ -127,9 +152,14 @@
127 152 agentBlock && setBlock(null);
128 153 // A class or pin left up freezes Quick Edit hover site-wide.
129 154 document
130 155 .querySelector('.wp-site-blocks')
131 - ?.classList.remove('extendify-agent-working', 'extendify-agent-busy');
156 + ?.classList.remove(
157 + 'extendify-agent-working',
158 + 'extendify-agent-busy',
159 + 'extendify-agent-waiting',
160 + 'extendify-agent-task',
161 + );
132 162 setCommittedSelection(null);
133 163 clearStatuses();
134 164 window.dispatchEvent(new Event('extendify-agent:remove-block-highlight'));
135 165 }, [setBlock, clearStatuses]);
@@ -273,10 +303,11 @@
273 303 }, [addMessage, pushStatus, whenFinishedToolProps, workflow]);
274 304
275 305 const handleSubmit = useCallback(
276 306 async (message, { hidden = false } = {}) => {
277 - // Suggestions reach the agent without the textarea; disabling it isn't enough.
307 + // chat-submit events skip the textarea; disabling it isn't enough.
278 308 if (useQuickEditStore.getState().selected) return;
309 + if (whenFinishedToolProps?.processing) return;
279 310 setWaitingOnToolOrUser(false);
280 311 agentWorking.current = false;
281 312 addMessage('message', { role: 'user', content: message, hidden });
282 313
@@ -292,9 +323,9 @@
292 323 label: whenFinishedTool?.labels?.cancel,
293 324 agent: workflow.agent,
294 325 workflowId: workflow.id,
295 326 answerId,
296 - suggestions: getSuggestions(),
327 + followUp: null,
297 328 });
298 329 }
299 330
300 331 // Let some phrases auto load workflows
@@ -345,9 +376,8 @@
345 376 setWorkflow,
346 377 workflow,
347 378 workflowData,
348 379 getAvailableWorkflows,
349 - getSuggestions,
350 380 ],
351 381 );
352 382
353 383 // Used to inject a workflow final state
@@ -423,9 +453,9 @@
423 453 addMessage('workflow', {
424 454 status: 'canceled',
425 455 agent: workflow?.agent,
426 456 workflowId: workflow?.id,
427 - suggestions: getSuggestions(),
457 + ...nextSteps(workflow?.id, 'canceled'),
428 458 });
429 459 return;
430 460 };
431 461 window.addEventListener('extendify-agent:cancel-workflow', handleCleanup);
@@ -443,9 +473,8 @@
443 473 setWorkflow,
444 474 addMessage,
445 475 workflow,
446 476 qaSuggestions,
447 - getSuggestions,
448 477 canType,
449 478 ]);
450 479
451 480 // Dispatching before chat-submit has a listener drops the workflow.
@@ -457,22 +486,39 @@
457 486 }),
458 487 );
459 488 }, []);
460 489
490 + // ChatMessages hides a confirm restored by a reload; left open, the workflow would sit unfinishable.
491 + useEffect(() => {
492 + const { getWorkflow, reloadedToolProps } = useWorkflowStore.getState();
493 + const { whenFinished, needsRedirect } = getWorkflow() ?? {};
494 + if (!reloadedToolProps?.id || !whenFinished?.component) return;
495 + if (whenFinished.canvas || needsRedirect?.()) return;
496 + window.dispatchEvent(new Event('extendify-agent:cancel-workflow'));
497 + }, []);
498 +
461 499 // Handle whenFinished component confirm/cancel
462 500 useEffect(() => {
463 501 const handleConfirm = async ({ detail }) => {
464 502 if (toolWorking.current) return;
465 - setWhenFinishedToolProps(null);
466 503 toolWorking.current = true;
467 504 const { data, whenFinishedToolProps, shouldRefreshPage, redirectUrl } =
468 505 detail ?? {};
506 + // Staged until the tool returns, so the chat shows it saving.
507 + setWhenFinishedToolProps(
508 + whenFinishedToolProps
509 + ? { ...whenFinishedToolProps, processing: true }
510 + : null,
511 + );
469 512 const { whenFinishedTool, answerId, redirectTo } =
470 513 whenFinishedToolProps?.agentResponse || {};
471 514 const { id, labels } = whenFinishedTool || {};
472 515 // Staged unanswered so its own component can show the run in progress.
473 516 const runMessageId = id ? addMessage('tool', { id, inputs: data }) : null;
474 - if (!hasRunComponent(id)) pushStatus('workflow-tool-processing');
517 + // In the chat, the saving state shows this label instead.
518 + if (!hasRunComponent(id) && workflow?.whenFinished?.canvas) {
519 + pushStatus('workflow-tool-processing', labels?.started);
520 + }
475 521 // Not all workflows have a tool at the end (e.g. tours)
476 522 const toolResponse = await callTool?.({ tool: id, inputs: data }).catch(
477 523 (error) => {
478 524 const { sessionId } = workflow || {};
@@ -488,8 +534,9 @@
488 534 return { error: { message: error?.message, code: error?.code } };
489 535 },
490 536 );
491 537 toolWorking.current = false;
538 + setWhenFinishedToolProps(null);
492 539 if (runMessageId) updateMessage(runMessageId, { result: toolResponse });
493 540 if (toolResponse?.refused) {
494 541 await new Promise((resolve) => setTimeout(resolve, 1000));
495 542 const refusalMessages = {
@@ -522,9 +569,15 @@
522 569 setLoop((prev) => prev + 1);
523 570 return;
524 571 }
525 572
526 - addSuggestions(whenFinishedToolProps.agentResponse?.recommendations);
573 + const url = getRedirectUrl(redirectTo, whenFinishedToolProps?.inputs);
574 + // Reloading here unmounts a run component still watching its ability finish.
575 + const refreshForAbility =
576 + isAbilityTool(id) && shouldRefreshPage !== false;
577 + const willReload =
578 + url || redirectUrl || shouldRefreshPage || refreshForAbility;
579 + // A card shown now would flash just before the reload.
527 580 addMessage('workflow', {
528 581 status: 'completed',
529 582 label: labels?.confirm,
530 583 agent: workflow.agent,
@@ -529,9 +582,9 @@
529 582 label: labels?.confirm,
530 583 agent: workflow.agent,
531 584 workflowId: workflow.id,
532 585 answerId,
533 - suggestions: getSuggestions(),
586 + ...(willReload ? {} : nextSteps(workflow.id, 'completed')),
534 587 });
535 588 // A chat message persists, so the next turn's model knows the save was partial.
536 589 const refusedCount = toolResponse?.refusedOperations?.length;
537 590 if (refusedCount) {
@@ -551,13 +604,9 @@
551 604 }
552 605 setWorkflow(null);
553 606 useCanvasStore.getState().endSession();
554 607
555 - const url = getRedirectUrl(redirectTo, whenFinishedToolProps?.inputs);
556 - const refreshForAbility = isAbilityTool(id);
557 - if (url || redirectUrl || shouldRefreshPage || refreshForAbility) {
558 - return doReload(url || redirectUrl);
559 - }
608 + if (willReload) return doReload(url || redirectUrl);
560 609 cleanup();
561 610 };
562 611 const handleCancel = ({ detail }) => {
563 612 if (toolWorking.current) return;
@@ -570,9 +619,9 @@
570 619 label: whenFinishedTool?.labels?.cancel,
571 620 agent: workflow.agent,
572 621 workflowId: workflow.id,
573 622 answerId,
574 - suggestions: getSuggestions(),
623 + ...nextSteps(workflow.id, 'canceled'),
575 624 });
576 625 setWorkflow(null);
577 626 useCanvasStore.getState().endSession();
578 627 cleanup();
@@ -597,18 +646,9 @@
597 646 handleCancel,
598 647 );
599 648 window.removeEventListener('extendify-agent:workflow-retry', handleRetry);
600 649 };
601 - }, [
602 - addMessage,
603 - pushStatus,
604 - popMessage,
605 - cleanup,
606 - setWorkflow,
607 - workflow,
608 - getSuggestions,
609 - addSuggestions,
610 - ]);
650 + }, [addMessage, pushStatus, popMessage, cleanup, setWorkflow, workflow]);
611 651
612 652 useEffect(() => {
613 653 const handleClose = () => setOpen(false);
614 654 const handleOpen = () => setOpen(true);
@@ -628,9 +668,26 @@
628 668 if (open) return;
629 669 if (block) setBlock(null);
630 670 }, [open, block, setBlock]);
631 671
672 + // A pick answers select-block's "which one?"; making the user also type stalls it.
673 + const previousBlock = useRef(block);
632 674 useEffect(() => {
675 + const picked = block && !previousBlock.current;
676 + previousBlock.current = block;
677 + if (!picked || workflow?.id !== 'select-block') return;
678 + if (!waitingOnToolOrUser || !canType || whenFinishedToolProps?.id) return;
679 + handleSubmit('This is the block I mean.', { hidden: true });
680 + }, [
681 + block,
682 + workflow?.id,
683 + waitingOnToolOrUser,
684 + canType,
685 + whenFinishedToolProps?.id,
686 + handleSubmit,
687 + ]);
688 +
689 + useEffect(() => {
633 690 if (waitingOnToolOrUser || !open || !workflow?.id) return;
634 691 // Some workflows require they dont change pages
635 692 const theyMoved = workflow?.startingPage !== window.location.href;
636 693 // Requires a block to be selected
@@ -641,9 +698,9 @@
641 698 addMessage('workflow', {
642 699 status: 'canceled',
643 700 agent: workflow.agent,
644 701 workflowId: workflow.id,
645 - suggestions: getSuggestions(),
702 + ...nextSteps(workflow.id, 'canceled'),
646 703 });
647 704 setWorkflow(null);
648 705 cleanup();
649 706 return;
@@ -740,9 +797,8 @@
740 797 status: 'completed',
741 798 inputs,
742 799 workflowId: workflow.id,
743 800 });
744 - addSuggestions(agentResponse.recommendations);
745 801 setWorkflow(null);
746 802 addMessage('workflow', {
747 803 status: 'completed',
748 804 agent: workflow.agent,
@@ -747,9 +803,9 @@
747 803 status: 'completed',
748 804 agent: workflow.agent,
749 805 workflowId: workflow.id,
750 806 answerId,
751 - suggestions: getSuggestions(),
807 + ...nextSteps(workflow.id, 'completed'),
752 808 });
753 809 cleanup();
754 810 }
755 811 return;
@@ -755,19 +811,21 @@
755 811 return;
756 812 }
757 813 // If we're done, it means the AI has the answer
758 814 if (agentResponse.status !== 'in-progress') {
759 - const { recommendations, status } = agentResponse;
760 - const isCompleted = status === 'completed';
761 - if (recommendations) addSuggestions(recommendations);
815 + const status =
816 + agentResponse.status === 'completed' ? 'completed' : 'canceled';
817 + // A reply that changed nothing isn't a finished task to follow up.
818 + const didWork =
819 + toolCallsThisRun(useChatStore.getState().messages).length > 0;
762 820 setWorkflow(null);
763 821 cleanup();
764 822 addMessage('workflow', {
765 - status: isCompleted ? 'completed' : 'canceled',
823 + status,
766 824 agent: workflow.agent,
767 825 workflowId: workflow.id,
768 826 answerId,
769 - suggestions: getSuggestions(),
827 + ...(didWork ? nextSteps(workflow.id, status) : { followUp: null }),
770 828 });
771 829 return;
772 830 }
773 831 if (sessionId && sessionId !== workflow.sessionId) {
@@ -787,9 +845,10 @@
787 845 setWaitingOnToolOrUser(true);
788 846 setCanType(false);
789 847 return;
790 848 }
791 - pushStatus('tool-started', labels?.started);
849 + // A reply sent with the tool clears the status, blanking the line.
850 + pushStatus('agent-working');
792 851 const toolData = await Promise.all([
793 852 callTool({ tool: id, inputs }),
794 853 new Promise((resolve) => setTimeout(resolve, 3000)),
795 854 ])
@@ -820,8 +879,9 @@
820 879 id,
821 880 inputs,
822 881 result: toolData,
823 882 label: labels?.confirm,
883 + started: labels?.started,
824 884 });
825 885 setWaitingOnToolOrUser(false);
826 886 agentWorking.current = false;
827 887 setLoop((prev) => prev + 1); // Trigger next loop
@@ -867,10 +927,8 @@
867 927 setWhenFinishedToolProps,
868 928 block,
869 929 setBlock,
870 930 findAgent,
871 - addSuggestions,
872 - getSuggestions,
873 931 ]);
874 932
875 933 useEffect(() => {
876 934 if (!canType) return;
@@ -876,9 +934,15 @@
876 934 if (!canType) return;
877 935 document.querySelector('#extendify-agent-chat-textarea')?.focus();
878 936 }, [canType]);
879 937
880 - const busy = !canType || !chatAvailable || workflow?.id;
938 + // A task waiting on a reply leaves the page pickable so the user can answer by pointing.
939 + const waiting =
940 + canType &&
941 + chatAvailable &&
942 + Boolean(workflow?.id) &&
943 + !whenFinishedToolProps?.id;
944 + const busy = !canType || !chatAvailable || (workflow?.id && !waiting);
881 945 // `busy` is true at rest; 429 is blocked, not working.
882 946 const working = !canType && chatAvailable;
883 947
884 948 return (
@@ -883,9 +947,14 @@
883 947
884 948 return (
885 949 <>
886 950 <Canvas />
887 - <Chat busy={busy} working={working}>
951 + <Chat
952 + busy={busy}
953 + working={working}
954 + waiting={waiting}
955 + taskActive={Boolean(workflow?.id)}
956 + >
888 957 <div className="relative z-50 flex h-full flex-col justify-between overflow-auto">
889 958 <ChatMessages
890 959 redirectComponent={
891 960 workflow?.needsRedirect?.() ? workflow.redirectComponent : null
@@ -900,11 +969,16 @@
900 969 }}
901 970 />
902 971 </div>
903 972 <div className="p-4 pb-2 pt-0">
973 + <StatusIndicator />
904 974 <ChatInput
905 975 disabled={
906 - !canType || !chatAvailable || !!qaSuggestions || leavingPage
976 + !canType ||
977 + !chatAvailable ||
978 + !!qaSuggestions ||
979 + leavingPage ||
980 + whenFinishedToolProps?.processing
907 981 }
908 982 handleSubmit={handleSubmit}
909 983 />
910 984 </div>