@personaai/react 0.1.2 → 0.3.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/dist/index.js CHANGED
@@ -41,15 +41,61 @@ function usePersonaContext() {
41
41
  }
42
42
 
43
43
  // src/hooks/useChat.ts
44
- import { useCallback, useRef, useState } from "react";
44
+ import { useCallback, useEffect, useRef, useState } from "react";
45
+ function isErrorToolContent(content) {
46
+ if (typeof content !== "string" || !content.trim().startsWith("{")) return false;
47
+ try {
48
+ return JSON.parse(content)?.status === "error";
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ function parsePresentedFile(content) {
54
+ try {
55
+ const parsed = JSON.parse(content);
56
+ if (parsed?.status !== "success" || typeof parsed.filePath !== "string") return null;
57
+ return {
58
+ path: parsed.filePath,
59
+ title: typeof parsed.title === "string" ? parsed.title : parsed.filePath,
60
+ description: typeof parsed.description === "string" ? parsed.description : ""
61
+ };
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ function normalizePendingInterrupt(pending) {
67
+ if (!pending || typeof pending !== "object") return null;
68
+ const p = pending;
69
+ if (p.kind === "hitl") {
70
+ return {
71
+ kind: "hitl",
72
+ actionRequests: p.value?.actionRequests ?? [],
73
+ reviewConfigs: p.value?.reviewConfigs ?? []
74
+ };
75
+ }
76
+ if (p.kind === "clarification") {
77
+ return {
78
+ kind: "clarification",
79
+ questions: p.value?.questions ?? []
80
+ };
81
+ }
82
+ return null;
83
+ }
45
84
  function useChat(options = {}) {
46
85
  const { defaultAgentId, fetchWithAuth } = usePersonaContext();
47
86
  const agentId = options.agentId || defaultAgentId;
87
+ const threadId = options.threadId;
48
88
  const [messages, setMessages] = useState(options.initialMessages || []);
49
89
  const [input, setInput] = useState("");
50
90
  const [isStreaming, setIsStreaming] = useState(false);
91
+ const [isLoadingHistory, setIsLoadingHistory] = useState(false);
51
92
  const [error, setError] = useState(null);
93
+ const [interrupt, setInterrupt] = useState(null);
94
+ const [files, setFiles] = useState({});
95
+ const [todos, setTodos] = useState([]);
96
+ const [presentedFile, setPresentedFile] = useState(null);
52
97
  const abortControllerRef = useRef(null);
98
+ const loadedThreadIdRef = useRef(void 0);
53
99
  const stop = useCallback(() => {
54
100
  if (abortControllerRef.current) {
55
101
  abortControllerRef.current.abort();
@@ -61,7 +107,50 @@ function useChat(options = {}) {
61
107
  stop();
62
108
  setMessages([]);
63
109
  setError(null);
110
+ setInterrupt(null);
111
+ setFiles({});
112
+ setTodos([]);
113
+ setPresentedFile(null);
64
114
  }, [stop]);
115
+ const loadThreadMessages = useCallback(
116
+ async (id) => {
117
+ setIsLoadingHistory(true);
118
+ setError(null);
119
+ try {
120
+ const res = await fetchWithAuth(`/threads/${id}/messages`);
121
+ if (!res.ok) throw new Error(`Failed to load thread history: ${res.statusText}`);
122
+ const body = await res.json();
123
+ const data = body?.data ?? body;
124
+ const raw = data?.messages ?? [];
125
+ const loaded = raw.map((m, i) => ({
126
+ id: m.id || `history-${id}-${i}`,
127
+ role: m.role,
128
+ content: m.content,
129
+ createdAt: /* @__PURE__ */ new Date(),
130
+ toolCalls: m.toolCalls
131
+ }));
132
+ setMessages(loaded);
133
+ setInterrupt(normalizePendingInterrupt(data?.pendingInterrupt));
134
+ setFiles(data?.state?.files ?? {});
135
+ setTodos(data?.state?.todos ?? []);
136
+ return loaded;
137
+ } catch (err) {
138
+ const errorObj = err instanceof Error ? err : new Error(String(err));
139
+ setError(errorObj);
140
+ return [];
141
+ } finally {
142
+ setIsLoadingHistory(false);
143
+ }
144
+ },
145
+ [fetchWithAuth]
146
+ );
147
+ useEffect(() => {
148
+ if (!threadId || isStreaming) return;
149
+ if (loadedThreadIdRef.current === threadId) return;
150
+ if (messages.length > 0) return;
151
+ loadedThreadIdRef.current = threadId;
152
+ void loadThreadMessages(threadId);
153
+ }, [threadId, isStreaming, messages.length, loadThreadMessages]);
65
154
  const sendMessage = useCallback(
66
155
  async (contentToSend, overrideOptions) => {
67
156
  const prompt = (contentToSend ?? input).trim();
@@ -93,6 +182,7 @@ function useChat(options = {}) {
93
182
  setInput("");
94
183
  setIsStreaming(true);
95
184
  setError(null);
185
+ setInterrupt(null);
96
186
  const controller = new AbortController();
97
187
  abortControllerRef.current = controller;
98
188
  try {
@@ -108,7 +198,8 @@ function useChat(options = {}) {
108
198
  body: JSON.stringify({
109
199
  agentId: targetAgentId,
110
200
  messages: payloadMessages,
111
- threadId: overrideOptions?.threadId || options.threadId
201
+ threadId: overrideOptions?.threadId || options.threadId,
202
+ resume: overrideOptions?.resume
112
203
  }),
113
204
  signal: controller.signal
114
205
  });
@@ -123,7 +214,15 @@ function useChat(options = {}) {
123
214
  const decoder = new TextDecoder("utf-8");
124
215
  let buffer = "";
125
216
  let accumulatedText = "";
217
+ let accumulatedReasoning = "";
126
218
  const toolCallsMap = /* @__PURE__ */ new Map();
219
+ const patchAssistant = (patch) => {
220
+ setMessages(
221
+ (prev) => prev.map(
222
+ (msg) => msg.id === assistantMessageId ? { ...msg, toolCalls: Array.from(toolCallsMap.values()), ...patch } : msg
223
+ )
224
+ );
225
+ };
127
226
  while (true) {
128
227
  const { done, value } = await reader.read();
129
228
  if (done) break;
@@ -140,47 +239,56 @@ function useChat(options = {}) {
140
239
  options.onEvent?.(event);
141
240
  if (event.type === "TEXT_MESSAGE_CHUNK" && event.delta) {
142
241
  accumulatedText += event.delta;
143
- setMessages(
144
- (prev) => prev.map(
145
- (msg) => msg.id === assistantMessageId ? {
146
- ...msg,
147
- content: accumulatedText,
148
- isStreaming: true,
149
- toolCalls: Array.from(toolCallsMap.values())
150
- } : msg
151
- )
152
- );
153
- } else if (event.type === "TOOL_CALL_START") {
154
- toolCallsMap.set(event.toolCallId, {
155
- toolCallId: event.toolCallId,
156
- toolName: event.toolName,
157
- args: ""
158
- });
159
- setMessages(
160
- (prev) => prev.map(
161
- (msg) => msg.id === assistantMessageId ? { ...msg, toolCalls: Array.from(toolCallsMap.values()) } : msg
162
- )
163
- );
164
- } else if (event.type === "TOOL_CALL_ARGS") {
242
+ patchAssistant({ content: accumulatedText, isStreaming: true });
243
+ } else if (event.type === "TOOL_CALL_CHUNK" && event.toolCallId) {
165
244
  const existing = toolCallsMap.get(event.toolCallId);
166
245
  if (existing) {
167
- existing.args = (existing.args || "") + event.delta;
168
- setMessages(
169
- (prev) => prev.map(
170
- (msg) => msg.id === assistantMessageId ? { ...msg, toolCalls: Array.from(toolCallsMap.values()) } : msg
171
- )
172
- );
246
+ existing.args = (existing.args || "") + (event.delta || "");
247
+ } else {
248
+ toolCallsMap.set(event.toolCallId, {
249
+ toolCallId: event.toolCallId,
250
+ toolName: event.toolCallName || "",
251
+ args: event.delta || ""
252
+ });
173
253
  }
254
+ patchAssistant({});
174
255
  } else if (event.type === "TOOL_CALL_RESULT") {
175
256
  const existing = toolCallsMap.get(event.toolCallId);
176
257
  if (existing) {
177
- existing.result = event.result;
178
- existing.isError = event.isError;
179
- setMessages(
180
- (prev) => prev.map(
181
- (msg) => msg.id === assistantMessageId ? { ...msg, toolCalls: Array.from(toolCallsMap.values()) } : msg
182
- )
183
- );
258
+ existing.result = event.content;
259
+ existing.isError = isErrorToolContent(event.content);
260
+ if (existing.toolName === "present_file" && !existing.isError) {
261
+ const presented = parsePresentedFile(event.content);
262
+ if (presented) setPresentedFile(presented);
263
+ }
264
+ patchAssistant({});
265
+ }
266
+ } else if (event.type === "STATE_SNAPSHOT") {
267
+ setFiles(event.snapshot.files);
268
+ setTodos(event.snapshot.todos);
269
+ } else if (event.type === "REASONING_MESSAGE_CONTENT") {
270
+ accumulatedReasoning += event.delta;
271
+ patchAssistant({ reasoning: accumulatedReasoning, isReasoning: true });
272
+ } else if (event.type === "REASONING_END") {
273
+ patchAssistant({ isReasoning: false });
274
+ } else if (event.type === "CUSTOM") {
275
+ if (event.name === "hitl_request") {
276
+ const value2 = event.value;
277
+ setInterrupt({
278
+ kind: "hitl",
279
+ actionRequests: value2.actionRequests,
280
+ reviewConfigs: value2.reviewConfigs
281
+ });
282
+ } else if (event.name === "clarification_request") {
283
+ const value2 = event.value;
284
+ setInterrupt({ kind: "clarification", questions: value2.questions });
285
+ } else if (event.name === "subagent_activity") {
286
+ const { toolCallId, ...entry } = event.value;
287
+ const existing = toolCallsMap.get(toolCallId);
288
+ if (existing) {
289
+ existing.subagentActivity = [...existing.subagentActivity || [], entry];
290
+ patchAssistant({});
291
+ }
184
292
  }
185
293
  } else if (event.type === "RUN_ERROR") {
186
294
  throw new Error(event.message || "Stream error from agent");
@@ -198,7 +306,8 @@ function useChat(options = {}) {
198
306
  content: accumulatedText,
199
307
  createdAt: /* @__PURE__ */ new Date(),
200
308
  isStreaming: false,
201
- toolCalls: Array.from(toolCallsMap.values())
309
+ toolCalls: Array.from(toolCallsMap.values()),
310
+ ...accumulatedReasoning ? { reasoning: accumulatedReasoning, isReasoning: false } : {}
202
311
  };
203
312
  setMessages(
204
313
  (prev) => prev.map((msg) => msg.id === assistantMessageId ? finalMessage : msg)
@@ -256,6 +365,11 @@ function useChat(options = {}) {
256
365
  setMessages(messages.slice(0, lastUserIndex));
257
366
  void sendMessage(lastUserMessage.content);
258
367
  }, [isStreaming, messages, sendMessage]);
368
+ const resumeInterrupt = useCallback(
369
+ (resume, displayContent) => sendMessage(displayContent, { resume }),
370
+ [sendMessage]
371
+ );
372
+ const dismissPresentedFile = useCallback(() => setPresentedFile(null), []);
259
373
  return {
260
374
  messages,
261
375
  input,
@@ -265,19 +379,27 @@ function useChat(options = {}) {
265
379
  sendMessage,
266
380
  isStreaming,
267
381
  isLoading: isStreaming,
382
+ isLoadingHistory,
268
383
  error,
384
+ interrupt,
385
+ resumeInterrupt,
386
+ files,
387
+ todos,
388
+ presentedFile,
389
+ dismissPresentedFile,
269
390
  stop,
270
391
  reload,
271
392
  clear,
272
- setMessages
393
+ setMessages,
394
+ loadThreadMessages
273
395
  };
274
396
  }
275
397
 
276
398
  // src/hooks/useMemory.ts
277
- import { useCallback as useCallback2, useEffect, useState as useState2 } from "react";
399
+ import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
278
400
  function useMemory(autoFetch = true) {
279
401
  const { fetchWithAuth } = usePersonaContext();
280
- const [memory, setMemory] = useState2({ user: [], agents: {} });
402
+ const [memory, setMemory] = useState2({ userFiles: [], agentMemories: [] });
281
403
  const [isLoading, setIsLoading] = useState2(false);
282
404
  const [error, setError] = useState2(null);
283
405
  const fetchMemory = useCallback2(async () => {
@@ -294,7 +416,7 @@ function useMemory(autoFetch = true) {
294
416
  } catch (err) {
295
417
  const errorObj = err instanceof Error ? err : new Error(String(err));
296
418
  setError(errorObj);
297
- return { user: [], agents: {} };
419
+ return { userFiles: [], agentMemories: [] };
298
420
  } finally {
299
421
  setIsLoading(false);
300
422
  }
@@ -337,7 +459,7 @@ function useMemory(autoFetch = true) {
337
459
  },
338
460
  [fetchWithAuth, fetchMemory]
339
461
  );
340
- useEffect(() => {
462
+ useEffect2(() => {
341
463
  if (autoFetch) {
342
464
  void fetchMemory();
343
465
  }
@@ -354,7 +476,7 @@ function useMemory(autoFetch = true) {
354
476
  }
355
477
 
356
478
  // src/hooks/useThreads.ts
357
- import { useCallback as useCallback3, useEffect as useEffect2, useState as useState3 } from "react";
479
+ import { useCallback as useCallback3, useEffect as useEffect3, useState as useState3 } from "react";
358
480
  function useThreads(autoFetch = true) {
359
481
  const { fetchWithAuth, defaultAgentId } = usePersonaContext();
360
482
  const [threads, setThreads] = useState3([]);
@@ -367,7 +489,7 @@ function useThreads(autoFetch = true) {
367
489
  const res = await fetchWithAuth("/threads");
368
490
  if (!res.ok) throw new Error(`Failed to list threads: ${res.statusText}`);
369
491
  const data = await res.json();
370
- const items = Array.isArray(data) ? data : data?.threads || data?.items || [];
492
+ const items = Array.isArray(data) ? data : data?.items || data?.threads || (Array.isArray(data?.data) ? data.data : void 0) || [];
371
493
  setThreads(items);
372
494
  return items;
373
495
  } catch (err) {
@@ -387,7 +509,8 @@ function useThreads(autoFetch = true) {
387
509
  body: JSON.stringify({ agentId: targetAgentId })
388
510
  });
389
511
  if (!res.ok) throw new Error(`Failed to create thread: ${res.statusText}`);
390
- const created = await res.json();
512
+ const data = await res.json();
513
+ const created = data?.data ?? data;
391
514
  void fetchThreads();
392
515
  return created;
393
516
  },
@@ -401,7 +524,57 @@ function useThreads(autoFetch = true) {
401
524
  },
402
525
  [fetchWithAuth]
403
526
  );
404
- useEffect2(() => {
527
+ const bulkDeleteThreads = useCallback3(
528
+ async (threadIds) => {
529
+ const res = await fetchWithAuth("/threads/bulk-delete", {
530
+ method: "POST",
531
+ headers: { "Content-Type": "application/json" },
532
+ body: JSON.stringify({ ids: threadIds })
533
+ });
534
+ if (!res.ok) throw new Error(`Failed to bulk-delete threads: ${res.statusText}`);
535
+ const data = await res.json();
536
+ const result = data?.data ?? data;
537
+ const deletedSet = new Set(result.deleted);
538
+ setThreads((prev) => prev.filter((t) => !deletedSet.has(t._id)));
539
+ return result;
540
+ },
541
+ [fetchWithAuth]
542
+ );
543
+ const deleteAllThreads = useCallback3(async () => {
544
+ const ids = threads.map((t) => t._id);
545
+ for (let i = 0; i < ids.length; i += 100) {
546
+ await bulkDeleteThreads(ids.slice(i, i + 100));
547
+ }
548
+ }, [threads, bulkDeleteThreads]);
549
+ const updateThread = useCallback3(
550
+ async (threadId, input) => {
551
+ const res = await fetchWithAuth(`/threads/${threadId}`, {
552
+ method: "PATCH",
553
+ headers: { "Content-Type": "application/json" },
554
+ body: JSON.stringify(input)
555
+ });
556
+ if (!res.ok) throw new Error(`Failed to update thread: ${res.statusText}`);
557
+ const data = await res.json();
558
+ const updated = data?.data ?? data;
559
+ setThreads((prev) => prev.map((t) => t._id === threadId ? { ...t, ...updated } : t));
560
+ return updated;
561
+ },
562
+ [fetchWithAuth]
563
+ );
564
+ const renameThread = useCallback3(
565
+ (threadId, title) => updateThread(threadId, { title }),
566
+ [updateThread]
567
+ );
568
+ const getThread = useCallback3(
569
+ async (threadId) => {
570
+ const res = await fetchWithAuth(`/threads/${threadId}`);
571
+ if (!res.ok) throw new Error(`Failed to fetch thread: ${res.statusText}`);
572
+ const data = await res.json();
573
+ return data?.data ?? data;
574
+ },
575
+ [fetchWithAuth]
576
+ );
577
+ useEffect3(() => {
405
578
  if (autoFetch) {
406
579
  void fetchThreads();
407
580
  }
@@ -412,12 +585,17 @@ function useThreads(autoFetch = true) {
412
585
  error,
413
586
  refetch: fetchThreads,
414
587
  createThread,
415
- deleteThread
588
+ deleteThread,
589
+ bulkDeleteThreads,
590
+ deleteAllThreads,
591
+ updateThread,
592
+ renameThread,
593
+ getThread
416
594
  };
417
595
  }
418
596
 
419
597
  // src/hooks/useFiles.ts
420
- import { useCallback as useCallback4, useEffect as useEffect3, useState as useState4 } from "react";
598
+ import { useCallback as useCallback4, useEffect as useEffect4, useState as useState4 } from "react";
421
599
  function useFiles(autoFetch = true) {
422
600
  const { fetchWithAuth } = usePersonaContext();
423
601
  const [files, setFiles] = useState4([]);
@@ -431,7 +609,7 @@ function useFiles(autoFetch = true) {
431
609
  const res = await fetchWithAuth("/files");
432
610
  if (!res.ok) throw new Error(`Failed to list files: ${res.statusText}`);
433
611
  const data = await res.json();
434
- const items = Array.isArray(data) ? data : data?.files || data?.items || [];
612
+ const items = Array.isArray(data) ? data : data?.items || data?.files || [];
435
613
  setFiles(items);
436
614
  return items;
437
615
  } catch (err) {
@@ -479,7 +657,21 @@ function useFiles(autoFetch = true) {
479
657
  async (fileId) => {
480
658
  const res = await fetchWithAuth(`/files/${fileId}`, { method: "DELETE" });
481
659
  if (!res.ok) throw new Error(`Failed to delete file: ${res.statusText}`);
482
- setFiles((prev) => prev.filter((f) => f._id !== fileId));
660
+ setFiles((prev) => prev.filter((f) => f.id !== fileId));
661
+ },
662
+ [fetchWithAuth]
663
+ );
664
+ const bulkDeleteFiles = useCallback4(
665
+ async (fileIds) => {
666
+ const res = await fetchWithAuth("/files/bulk-delete", {
667
+ method: "POST",
668
+ headers: { "Content-Type": "application/json" },
669
+ body: JSON.stringify({ ids: fileIds })
670
+ });
671
+ if (!res.ok) throw new Error(`Failed to bulk-delete files: ${res.statusText}`);
672
+ const idSet = new Set(fileIds);
673
+ setFiles((prev) => prev.filter((f) => !idSet.has(f.id)));
674
+ return await res.json();
483
675
  },
484
676
  [fetchWithAuth]
485
677
  );
@@ -489,7 +681,7 @@ function useFiles(autoFetch = true) {
489
681
  },
490
682
  []
491
683
  );
492
- useEffect3(() => {
684
+ useEffect4(() => {
493
685
  if (autoFetch) {
494
686
  void fetchFiles();
495
687
  }
@@ -502,12 +694,13 @@ function useFiles(autoFetch = true) {
502
694
  refetch: fetchFiles,
503
695
  uploadFile,
504
696
  deleteFile,
697
+ bulkDeleteFiles,
505
698
  getDownloadUrl
506
699
  };
507
700
  }
508
701
 
509
702
  // src/hooks/useAgents.ts
510
- import { useCallback as useCallback5, useEffect as useEffect4, useState as useState5 } from "react";
703
+ import { useCallback as useCallback5, useEffect as useEffect5, useState as useState5 } from "react";
511
704
  function useAgents(autoFetch = true) {
512
705
  const { fetchWithAuth } = usePersonaContext();
513
706
  const [agents, setAgents] = useState5([]);
@@ -520,7 +713,7 @@ function useAgents(autoFetch = true) {
520
713
  const res = await fetchWithAuth("/agents");
521
714
  if (!res.ok) throw new Error(`Failed to list agents: ${res.statusText}`);
522
715
  const data = await res.json();
523
- const items = Array.isArray(data) ? data : data?.agents || data?.items || [];
716
+ const items = Array.isArray(data) ? data : data?.items || data?.agents || [];
524
717
  setAgents(items);
525
718
  return items;
526
719
  } catch (err) {
@@ -531,7 +724,7 @@ function useAgents(autoFetch = true) {
531
724
  setIsLoading(false);
532
725
  }
533
726
  }, [fetchWithAuth]);
534
- useEffect4(() => {
727
+ useEffect5(() => {
535
728
  if (autoFetch) {
536
729
  void fetchAgents();
537
730
  }
@@ -545,7 +738,7 @@ function useAgents(autoFetch = true) {
545
738
  }
546
739
 
547
740
  // src/hooks/useConnection.ts
548
- import { useCallback as useCallback6, useEffect as useEffect5, useState as useState6 } from "react";
741
+ import { useCallback as useCallback6, useEffect as useEffect6, useState as useState6 } from "react";
549
742
  function useConnection(autoCheck = true) {
550
743
  const { fetchWithAuth } = usePersonaContext();
551
744
  const [health, setHealth] = useState6(null);
@@ -570,7 +763,7 @@ function useConnection(autoCheck = true) {
570
763
  setIsLoading(false);
571
764
  }
572
765
  }, [fetchWithAuth]);
573
- useEffect5(() => {
766
+ useEffect6(() => {
574
767
  if (autoCheck) {
575
768
  void checkHealth();
576
769
  }
@@ -584,7 +777,7 @@ function useConnection(autoCheck = true) {
584
777
  }
585
778
 
586
779
  // src/index.ts
587
- var VERSION = "0.1.2";
780
+ var VERSION = "0.2.0";
588
781
  export {
589
782
  PersonaProvider,
590
783
  VERSION,