| @@ -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}`, |