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/state/chat.js +69 -32 3.1.1 → trunk View file →
@@ -1,43 +1,32 @@
1 -import { isChangeSiteDesignWorkflowAvailable, makeId } from '@agent/lib/util';
1 +import { buildToolMessages } from '@agent/lib/tool-messages';
2 +import { makeId } from '@agent/lib/util';
3 +import { useStatusStore } from '@agent/state/status';
2 4 import apiFetch from '@wordpress/api-fetch';
3 -import { __ } from '@wordpress/i18n';
4 5 import { create } from 'zustand';
5 6 import { createJSONStorage, devtools, persist } from 'zustand/middleware';
6 7
7 8 const { chatHistory } = window.extAgentData;
8 9
9 -const welcomeMessage = [
10 - {
11 - id: 1,
12 - type: 'message',
13 - details: {
14 - role: 'assistant',
15 - // translators: this is the initial message in the agent chat, welcoming the user. Keep it short and friendly and follow the same markdown format and emoji.
16 - content: isChangeSiteDesignWorkflowAvailable()
17 - ? __(
18 - '#### Your site is ready 🎉\nWant to explore other website designs?',
19 - 'extendify-local',
20 - )
21 - : __(
22 - '#### Your site is ready 🎉\nWant to explore other site colors?',
23 - 'extendify-local',
24 - ),
25 - },
26 - },
27 -];
28 10 const state = (set, get) => ({
29 - messages: chatHistory?.length ? chatHistory.toReversed() : welcomeMessage,
30 - // Messages sent to the api, user and assistant only. Up until the last workflow
31 - getMessagesForAI: () => {
11 + messages: chatHistory?.length ? chatHistory.toReversed() : [],
12 + // API messages, back to the last finished workflow.
13 + getCurrentMessages: ({ includeTools = true } = {}) => {
32 14 const messages = [];
33 15 let foundUserMessage = false;
34 16 for (const { type, details } of get().messages.toReversed()) {
35 - const finished =
36 - ['completed', 'canceled'].includes(details.status) ||
37 - (['status'].includes(type) && details.type === 'workflow-canceled');
17 + const finished = ['completed', 'canceled'].includes(details.status);
38 18 if (type === 'workflow' && finished) break;
39 19 if (type === 'workflow-component' && finished) break;
20 + if (type === 'message' && details.error) break;
21 + // A call with no result leaves the model answering a phantom.
22 + if (type === 'tool' && !('result' in details)) continue;
23 + if (type === 'tool' && includeTools) {
24 + // buildToolMessages returns [call, result]; push reversed so the
25 + // final toReversed() restores call-before-result order.
26 + for (const m of buildToolMessages(details).toReversed())
27 + messages.push(m);
28 + }
40 29 // This prevents a loop of assistant messages from being at the end
41 30 if (type === 'message' && details.role === 'user') {
42 31 foundUserMessage = true;
43 32 }
@@ -45,8 +34,37 @@
45 34 if (type === 'message') messages.push(details);
46 35 }
47 36 return messages.toReversed();
48 37 },
38 + // Most recent consecutive runs — another workflow in between resets the
39 + // memory.
40 + getMessagesFor: (workflowId) => {
41 + if (!workflowId) return [];
42 + const segments = [];
43 + let segment = [];
44 + for (const { type, details } of get().messages) {
45 + const finished = ['completed', 'canceled'].includes(details.status);
46 + if (['workflow', 'workflow-component'].includes(type) && finished) {
47 + segments.push({ workflowId: details.workflowId, segment });
48 + segment = [];
49 + continue;
50 + }
51 + // An error ended a run without a marker; what precedes it is dead.
52 + if (type === 'message' && details.error) {
53 + segment = [];
54 + continue;
55 + }
56 + if (type === 'tool' && 'result' in details) {
57 + segment.push(...buildToolMessages(details, { summarize: true }));
58 + continue;
59 + }
60 + if (type === 'message') segment.push(details);
61 + }
62 + const broken = segments.findLastIndex(
63 + (finished) => finished.workflowId !== workflowId,
64 + );
65 + return segments.slice(broken + 1).flatMap(({ segment }) => segment);
66 + },
49 67 getLastAssistantMessage: () =>
50 68 get()?.messages?.findLast(
51 69 (message) =>
52 70 message.type === 'message' && message.details?.role === 'assistant',
@@ -54,21 +72,30 @@
54 72 hasMessages: () => get().messages.length > 0,
55 73 addMessage: (type, details) => {
56 74 const id = makeId();
57 75 set((state) => {
58 - // max 150 messages
59 - const max = Math.max(0, state.messages.length - 149);
76 + // max 250 messages
77 + const max = Math.max(0, state.messages.length - 249);
60 78 const next = { id, type, details };
61 79 return {
62 80 // { id: 1, type: message, details: { role: 'user', content: 'Hello' } }
63 81 // { id: 2, type: message, details: { role: 'assistant', content: 'Hi there!' } }
64 82 // { id: 3, type: workflow, details: { name: 'Workflow 1' } }
65 - // { id: 5, type: status, details: { type: 'calling-agent' }
66 83 messages: [...state.messages.toSpliced(0, max), next],
67 84 };
68 85 });
86 + // A real message supersedes any in-flight progress status.
87 + useStatusStore.getState().clearStatuses();
69 88 return id;
70 89 },
90 + updateMessage: (id, details) =>
91 + set((state) => ({
92 + messages: state.messages.map((message) =>
93 + message.id === id
94 + ? { ...message, details: { ...message.details, ...details } }
95 + : message,
96 + ),
97 + })),
71 98 // pop messages all the way back to the last agent message
72 99 popMessage: () => {
73 100 set((state) => ({
74 101 messages: state.messages?.slice(0, -1) || [],
@@ -77,13 +104,23 @@
77 104 clearMessages: () => set({ messages: [] }),
78 105 });
79 106
80 107 const path = '/extendify/v1/agent/chat-events';
108 +let lastSave = Promise.resolve();
81 109 const storage = {
82 110 getItem: async () => await apiFetch({ path }),
83 - setItem: async (_name, state) =>
84 - await apiFetch({ path, method: 'POST', data: { state } }),
111 + setItem: (_name, state) => {
112 + lastSave = apiFetch({ path, method: 'POST', data: { state } });
113 + return lastSave;
114 + },
85 115 };
116 +
117 +// Await before navigating — an aborted in-flight save loses the newest messages.
118 +export const flushChatStorage = (timeout = 5000) =>
119 + Promise.race([
120 + lastSave.catch(() => null),
121 + new Promise((resolve) => setTimeout(resolve, timeout)),
122 + ]);
86 123
87 124 export const useChatStore = create()(
88 125 persist(devtools(state, { name: 'Extendify Agent Chat' }), {
89 126 name: `extendify-agent-chat-${window.extSharedData.siteId}`,