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 +42 -29 3.1.2 → trunk View file →
@@ -1,35 +1,15 @@
1 1 import { buildToolMessages } from '@agent/lib/tool-messages';
2 -import { isChangeSiteDesignWorkflowAvailable, makeId } from '@agent/lib/util';
2 +import { makeId } from '@agent/lib/util';
3 3 import { useStatusStore } from '@agent/state/status';
4 4 import apiFetch from '@wordpress/api-fetch';
5 -import { __ } from '@wordpress/i18n';
6 5 import { create } from 'zustand';
7 6 import { createJSONStorage, devtools, persist } from 'zustand/middleware';
8 7
9 8 const { chatHistory } = window.extAgentData;
10 9
11 -const welcomeMessage = [
12 - {
13 - id: 1,
14 - type: 'message',
15 - details: {
16 - role: 'assistant',
17 - // 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.
18 - content: isChangeSiteDesignWorkflowAvailable()
19 - ? __(
20 - '#### Your site is ready 🎉\nWant to explore other website designs?',
21 - 'extendify-local',
22 - )
23 - : __(
24 - '#### Your site is ready 🎉\nWant to explore other site colors?',
25 - 'extendify-local',
26 - ),
27 - },
28 - },
29 -];
30 10 const state = (set, get) => ({
31 - messages: chatHistory?.length ? chatHistory.toReversed() : welcomeMessage,
11 + messages: chatHistory?.length ? chatHistory.toReversed() : [],
32 12 // API messages, back to the last finished workflow.
33 13 getCurrentMessages: ({ includeTools = true } = {}) => {
34 14 const messages = [];
35 15 let foundUserMessage = false;
@@ -36,8 +16,11 @@
36 16 for (const { type, details } of get().messages.toReversed()) {
37 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;
40 23 if (type === 'tool' && includeTools) {
41 24 // buildToolMessages returns [call, result]; push reversed so the
42 25 // final toReversed() restores call-before-result order.
43 26 for (const m of buildToolMessages(details).toReversed())
@@ -51,24 +34,36 @@
51 34 if (type === 'message') messages.push(details);
52 35 }
53 36 return messages.toReversed();
54 37 },
55 - // API messages from every finished run of the given workflow.
38 + // Most recent consecutive runs — another workflow in between resets the
39 + // memory.
56 40 getMessagesFor: (workflowId) => {
57 41 if (!workflowId) return [];
58 - const messages = [];
42 + const segments = [];
59 43 let segment = [];
60 44 for (const { type, details } of get().messages) {
61 45 const finished = ['completed', 'canceled'].includes(details.status);
62 46 if (['workflow', 'workflow-component'].includes(type) && finished) {
63 - if (details.workflowId === workflowId) messages.push(...segment);
47 + segments.push({ workflowId: details.workflowId, segment });
64 48 segment = [];
65 49 continue;
66 50 }
67 - if (type === 'tool') segment.push(...buildToolMessages(details));
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 + }
68 60 if (type === 'message') segment.push(details);
69 61 }
70 - return messages;
62 + const broken = segments.findLastIndex(
63 + (finished) => finished.workflowId !== workflowId,
64 + );
65 + return segments.slice(broken + 1).flatMap(({ segment }) => segment);
71 66 },
72 67 getLastAssistantMessage: () =>
73 68 get()?.messages?.findLast(
74 69 (message) =>
@@ -91,8 +86,16 @@
91 86 // A real message supersedes any in-flight progress status.
92 87 useStatusStore.getState().clearStatuses();
93 88 return id;
94 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 + })),
95 98 // pop messages all the way back to the last agent message
96 99 popMessage: () => {
97 100 set((state) => ({
98 101 messages: state.messages?.slice(0, -1) || [],
@@ -101,13 +104,23 @@
101 104 clearMessages: () => set({ messages: [] }),
102 105 });
103 106
104 107 const path = '/extendify/v1/agent/chat-events';
108 +let lastSave = Promise.resolve();
105 109 const storage = {
106 110 getItem: async () => await apiFetch({ path }),
107 - setItem: async (_name, state) =>
108 - await apiFetch({ path, method: 'POST', data: { state } }),
111 + setItem: (_name, state) => {
112 + lastSave = apiFetch({ path, method: 'POST', data: { state } });
113 + return lastSave;
114 + },
109 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 + ]);
110 123
111 124 export const useChatStore = create()(
112 125 persist(devtools(state, { name: 'Extendify Agent Chat' }), {
113 126 name: `extendify-agent-chat-${window.extSharedData.siteId}`,