@economic/agents-react 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -123,22 +123,27 @@ function Chat({ userId, authToken }: { userId: string; authToken: string }) {
123
123
  | Field | Type | Description |
124
124
  | ----------------- | ----------------------- | --------------------------------------------------------------------------- |
125
125
  | `status` | `AgentConnectionStatus` | Connection status of the assistant, or of the active chat once one is open. |
126
- | `chats` | `ChatSummary[]` | The user's chats, most recently updated first. |
126
+ | `chats` | `ChatSummary[]` | Started chats plus the active empty draft, most recently updated first. |
127
127
  | `currentChatName` | `string \| undefined` | Id of the active chat, if any. |
128
- | `assistant` | `AssistantActions` | `getChats`, `createChat`, `openChat`, `deleteChat`. |
128
+ | `assistant` | `AssistantActions` | `getChats`, `createChat`, `openChat`, `deleteChat`, `renameChat`. |
129
129
  | `chat` | `useChat` result | The active chat — `chat.chat` is the message interface (see below). |
130
130
 
131
131
  `assistant` actions:
132
132
 
133
- | Action | Signature | Description |
134
- | -------------------- | ----------------------------------- | ------------------------------------------------ |
135
- | `createChat()` | `() => Promise<string>` | Creates a chat, makes it active, returns its id. |
136
- | `openChat(chatId)` | `(chatId: string) => void` | Switches the active chat. |
137
- | `getChats()` | `() => Promise<ChatSummary[]>` | Refreshes and returns the chat list. |
138
- | `deleteChat(chatId)` | `(chatId: string) => Promise<void>` | Deletes a chat and clears the active selection. |
133
+ | Action | Signature | Description |
134
+ | -------------------- | -------------------------------------------------- | ------------------------------------------------ |
135
+ | `createChat()` | `() => Promise<string>` | Creates a chat, makes it active, returns its id. |
136
+ | `openChat(chatId)` | `(chatId: string) => void` | Switches the active chat. |
137
+ | `getChats()` | `() => Promise<ChatSummary[]>` | Refreshes and returns the chat list. |
138
+ | `deleteChat(chatId)` | `(chatId: string) => Promise<void>` | Deletes a chat and clears the active selection. |
139
+ | `renameChat(chatId)` | `(chatId: string, title: string) => Promise<void>` | Renames a registered chat. |
139
140
 
140
141
  > Titles and summaries are generated server-side a few seconds after the first message, so refetch with `getChats()` shortly after sending the opening message if you want the new title to appear.
141
142
 
143
+ The active empty chat is represented locally until its first turn registers it on the server.
144
+
145
+ After the active draft's first turn finishes, `useAssistant` refreshes the chat list automatically so the temporary entry is replaced by its persisted title and summary.
146
+
142
147
  ---
143
148
 
144
149
  ## Options
@@ -174,6 +179,7 @@ type AgentConnectionState = {
174
179
 
175
180
  type ChatSummary = {
176
181
  name: string;
182
+ isTemporary?: boolean; // true for the active chat before its first turn is registered
177
183
  title?: string;
178
184
  summary?: string;
179
185
  created_at: number;
package/dist/index.d.mts CHANGED
@@ -10,7 +10,8 @@ type Chat = ReturnType<typeof useAgentChat>;
10
10
  type AgentConnectionStatus = "connecting" | "connected" | "disconnected" | "unauthorized" | "error";
11
11
  type AgentConnectionType = "agent" | "chat" | "assistant";
12
12
  type ChatSummary = {
13
- name: string;
13
+ name: string; /** True when this entry only represents the active, unregistered chat facet. */
14
+ isTemporary?: boolean;
14
15
  title?: string;
15
16
  summary?: string;
16
17
  title_set_by_user?: number;
package/dist/index.mjs CHANGED
@@ -29,10 +29,56 @@ function getCloseReason(code) {
29
29
  1015: "tls-handshake-failure"
30
30
  }[code] ?? "unknown";
31
31
  }
32
+ function isSameConnectionTarget(a, b) {
33
+ if (a.enabled !== b.enabled || a.host !== b.host || a.agentName !== b.agentName || a.actorId !== b.actorId || a.name !== b.name || a.authToken !== b.authToken || a.sub?.length !== b.sub?.length) return false;
34
+ return a.sub?.every((subAgent, index) => subAgent.agent === b.sub?.[index]?.agent && subAgent.name === b.sub?.[index]?.name) ?? true;
35
+ }
32
36
  function useAgent(options) {
33
37
  const { enabled, host, agentName, actorId, name, sub, authToken, onError, onOpen, onClose, onMessage } = options;
34
- const [type, setType] = useState(void 0);
35
- const [status, setStatus] = useState("connecting");
38
+ const target = {
39
+ enabled,
40
+ host,
41
+ agentName,
42
+ actorId,
43
+ name,
44
+ sub,
45
+ authToken
46
+ };
47
+ const connection = useRef({
48
+ id: 0,
49
+ target
50
+ });
51
+ if (!isSameConnectionTarget(connection.current.target, target)) connection.current = {
52
+ id: connection.current.id + 1,
53
+ target
54
+ };
55
+ const connectionId = connection.current.id;
56
+ const currentConnectionId = useRef(connectionId);
57
+ currentConnectionId.current = connectionId;
58
+ const [connectionType, setConnectionType] = useState();
59
+ const type = connectionType?.connectionId === connectionId ? connectionType.type : void 0;
60
+ const [connectionStatus, setConnectionStatus] = useState({
61
+ connectionId,
62
+ status: "connecting"
63
+ });
64
+ const status = connectionStatus.connectionId === connectionId ? connectionStatus.status : "connecting";
65
+ const connectedConnectionId = useRef(void 0);
66
+ const setStatus = (nextStatus) => {
67
+ if (currentConnectionId.current !== connectionId) return;
68
+ if (nextStatus === "connected") connectedConnectionId.current = connectionId;
69
+ else if (nextStatus === "disconnected" && connectedConnectionId.current !== connectionId) return;
70
+ setConnectionStatus({
71
+ connectionId,
72
+ status: nextStatus
73
+ });
74
+ };
75
+ const setType = (nextType) => {
76
+ if (currentConnectionId.current === connectionId) setConnectionType({
77
+ connectionId,
78
+ type: nextType
79
+ });
80
+ };
81
+ const currentAgent = useRef(null);
36
82
  const agent = useAgent$1({
37
83
  enabled,
38
84
  host,
@@ -43,7 +89,7 @@ function useAgent(options) {
43
89
  onOpen?.(event);
44
90
  },
45
91
  onClose: (event) => {
46
- if (event.code >= 3e3) setStatus("unauthorized");
92
+ if (!event.target || event.target === currentAgent.current) if (event.code >= 3e3) setStatus("unauthorized");
47
93
  else {
48
94
  setStatus("disconnected");
49
95
  if (!event.wasClean) console.warn("[useAgent] WebSocket closed unexpectedly", {
@@ -57,18 +103,19 @@ function useAgent(options) {
57
103
  },
58
104
  onMessage: (event) => {
59
105
  const msg = JSON.parse(event.data);
60
- if (msg.type === "connection_status") {
106
+ if (msg.type === "connection_status" && (!event.target || event.target === currentAgent.current)) {
61
107
  if (msg.status === "unauthorized") agent.close(3e3);
62
108
  setStatus(msg.status);
63
- } else if (msg.type === "agent_type") setType(msg.agentType);
109
+ } else if (msg.type === "agent_type" && (!event.target || event.target === currentAgent.current)) setType(msg.agentType);
64
110
  onMessage?.(event);
65
111
  },
66
112
  onError: (event) => {
67
- setStatus("error");
113
+ if (!event.target || event.target === currentAgent.current) setStatus("error");
68
114
  onError?.(event);
69
115
  },
70
116
  ...getTokenArgs(authToken)
71
117
  });
118
+ currentAgent.current = agent;
72
119
  return {
73
120
  agent,
74
121
  status,
@@ -130,6 +177,16 @@ function useAssistant(options) {
130
177
  const { toolContext, welcomeMessage, initialChatId, ...agentOptions } = options;
131
178
  const [chatId, setChatId] = useState(initialChatId);
132
179
  const [chats, setChats] = useState([]);
180
+ const [draftChat, setDraftChat] = useState(() => {
181
+ if (!initialChatId) return void 0;
182
+ const now = Date.now();
183
+ return {
184
+ name: initialChatId,
185
+ isTemporary: true,
186
+ created_at: now,
187
+ updated_at: now
188
+ };
189
+ });
133
190
  const [subAgentName, setSubAgentName] = useState(void 0);
134
191
  const { agent: assistant, status: assistantStatus } = useAgent({
135
192
  ...agentOptions,
@@ -148,8 +205,29 @@ function useAssistant(options) {
148
205
  return c;
149
206
  });
150
207
  setChats(fetched);
208
+ setDraftChat((current) => {
209
+ if (current && fetched.some((chat) => chat.name === current.name)) return;
210
+ return current;
211
+ });
151
212
  return fetched;
152
213
  }, [assistant]);
214
+ const refreshChatsRef = useRef(refreshChats);
215
+ refreshChatsRef.current = refreshChats;
216
+ const draftRefreshTimeouts = useRef(/* @__PURE__ */ new Map());
217
+ const scheduleDraftRefresh = useCallback((draftId) => {
218
+ const existingTimeout = draftRefreshTimeouts.current.get(draftId);
219
+ if (existingTimeout) clearTimeout(existingTimeout);
220
+ const timeout = setTimeout(() => {
221
+ draftRefreshTimeouts.current.delete(draftId);
222
+ refreshChatsRef.current().catch(console.error);
223
+ }, 3e3);
224
+ draftRefreshTimeouts.current.set(draftId, timeout);
225
+ }, []);
226
+ useEffect(() => {
227
+ return () => {
228
+ for (const timeout of draftRefreshTimeouts.current.values()) clearTimeout(timeout);
229
+ };
230
+ }, []);
153
231
  useEffect(() => {
154
232
  if (assistantStatus === "connected") refreshChats().catch(console.error);
155
233
  }, [assistantStatus, refreshChats]);
@@ -157,17 +235,26 @@ function useAssistant(options) {
157
235
  return refreshChats();
158
236
  };
159
237
  const createChat = async () => {
238
+ if (draftChat && draftChat.name === chatId) scheduleDraftRefresh(draftChat.name);
160
239
  const newChatId = await assistant.call("createChat");
240
+ const now = Date.now();
161
241
  setChatId(newChatId);
162
- await refreshChats();
242
+ setDraftChat({
243
+ name: newChatId,
244
+ isTemporary: true,
245
+ created_at: now,
246
+ updated_at: now
247
+ });
163
248
  return newChatId;
164
249
  };
165
- const openChat = (chatId) => {
166
- setChatId(chatId);
250
+ const openChat = (nextChatId) => {
251
+ if (draftChat && draftChat.name === chatId) scheduleDraftRefresh(draftChat.name);
252
+ setChatId(nextChatId);
167
253
  };
168
254
  const deleteChat = async (chatId) => {
169
255
  await assistant.call("deleteChat", [chatId]);
170
256
  setChatId(void 0);
257
+ setDraftChat((current) => current?.name === chatId ? void 0 : current);
171
258
  await refreshChats();
172
259
  };
173
260
  const renameChat = async (chatId, title) => {
@@ -187,10 +274,24 @@ function useAssistant(options) {
187
274
  toolContext,
188
275
  welcomeMessage
189
276
  });
277
+ const previousChatStatus = useRef(chat.chat.status);
278
+ useEffect(() => {
279
+ const wasRunning = previousChatStatus.current === "submitted" || previousChatStatus.current === "streaming";
280
+ const hasFinished = chat.chat.status === "ready" || chat.chat.status === "error";
281
+ previousChatStatus.current = chat.chat.status;
282
+ if (!draftChat || draftChat.name !== chatId || !wasRunning || !hasFinished) return;
283
+ scheduleDraftRefresh(draftChat.name);
284
+ }, [
285
+ chat.chat.status,
286
+ chatId,
287
+ draftChat,
288
+ scheduleDraftRefresh
289
+ ]);
290
+ const status = isChatEnabled && chat.status !== "connecting" ? chat.status : assistantStatus;
190
291
  return {
191
292
  currentChatName: chatId,
192
- chats,
193
- status: isChatEnabled && chat.status !== "connecting" ? chat.status : assistantStatus,
293
+ chats: draftChat && draftChat.name === chatId && !chats.some((persistedChat) => persistedChat.name === draftChat.name) ? [draftChat, ...chats] : chats,
294
+ status,
194
295
  assistant: {
195
296
  agent: assistant,
196
297
  getChats,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents-react",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"