@personaai/react 0.3.5 → 0.5.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
@@ -42,6 +42,181 @@ function usePersonaContext() {
42
42
 
43
43
  // src/hooks/useChat.ts
44
44
  import { useCallback, useEffect, useRef, useState } from "react";
45
+
46
+ // src/streaming.ts
47
+ function supportsStreamingFetch() {
48
+ if (typeof navigator !== "undefined" && navigator.product === "ReactNative") {
49
+ return false;
50
+ }
51
+ return typeof fetch !== "undefined" && typeof ReadableStream !== "undefined";
52
+ }
53
+ function fetchReader(response, controller) {
54
+ const reader = response.body.getReader();
55
+ const decoder = new TextDecoder("utf-8");
56
+ return {
57
+ async read() {
58
+ const { done, value } = await reader.read();
59
+ if (done) return { done: true };
60
+ return { done: false, value: decoder.decode(value, { stream: true }) };
61
+ },
62
+ cancel() {
63
+ controller.abort();
64
+ void reader.cancel().catch(() => {
65
+ });
66
+ }
67
+ };
68
+ }
69
+ function xhrStream(opts) {
70
+ return new Promise((resolveStream, rejectStream) => {
71
+ const xhr = new XMLHttpRequest();
72
+ xhr.open("POST", opts.url);
73
+ for (const [key, val] of Object.entries(opts.headers)) xhr.setRequestHeader(key, val);
74
+ let consumed = 0;
75
+ let finished = false;
76
+ let failure = null;
77
+ const queue = [];
78
+ let waiting = null;
79
+ let waitingReject = null;
80
+ let headersResolved = false;
81
+ const pump = () => {
82
+ const text = xhr.responseText;
83
+ if (text.length <= consumed) return;
84
+ const chunk = text.slice(consumed);
85
+ consumed = text.length;
86
+ if (waiting) {
87
+ const resolve = waiting;
88
+ waiting = null;
89
+ waitingReject = null;
90
+ resolve({ done: false, value: chunk });
91
+ } else {
92
+ queue.push(chunk);
93
+ }
94
+ };
95
+ const finish = (err) => {
96
+ if (finished) return;
97
+ finished = true;
98
+ failure = err ?? null;
99
+ if (waiting) {
100
+ const resolve = waiting;
101
+ const reject = waitingReject;
102
+ waiting = null;
103
+ waitingReject = null;
104
+ if (err && reject) reject(err);
105
+ else resolve({ done: true });
106
+ }
107
+ };
108
+ const reader = {
109
+ read() {
110
+ if (queue.length > 0) {
111
+ return Promise.resolve({ done: false, value: queue.shift() });
112
+ }
113
+ if (finished) {
114
+ return failure ? Promise.reject(failure) : Promise.resolve({ done: true });
115
+ }
116
+ return new Promise((resolve, reject) => {
117
+ waiting = resolve;
118
+ waitingReject = reject;
119
+ });
120
+ },
121
+ cancel() {
122
+ finish();
123
+ xhr.abort();
124
+ }
125
+ };
126
+ const resolveHeaders = () => {
127
+ if (headersResolved) return;
128
+ headersResolved = true;
129
+ const ok = xhr.status >= 200 && xhr.status < 300;
130
+ resolveStream({
131
+ status: xhr.status,
132
+ ok,
133
+ getHeader: (name) => xhr.getResponseHeader(name),
134
+ reader
135
+ });
136
+ };
137
+ xhr.onreadystatechange = () => {
138
+ if (xhr.readyState === 2) {
139
+ resolveHeaders();
140
+ } else if (xhr.readyState === 3) {
141
+ resolveHeaders();
142
+ pump();
143
+ } else if (xhr.readyState === 4) {
144
+ resolveHeaders();
145
+ pump();
146
+ finish();
147
+ }
148
+ };
149
+ xhr.onerror = () => {
150
+ const err = new Error("Network request failed");
151
+ if (!headersResolved) rejectStream(err);
152
+ finish(err);
153
+ };
154
+ xhr.ontimeout = () => {
155
+ const err = new Error("Network request timed out");
156
+ if (!headersResolved) rejectStream(err);
157
+ finish(err);
158
+ };
159
+ if (opts.signal) {
160
+ if (opts.signal.aborted) {
161
+ xhr.abort();
162
+ finish();
163
+ } else {
164
+ opts.signal.addEventListener("abort", () => {
165
+ finish();
166
+ xhr.abort();
167
+ });
168
+ }
169
+ }
170
+ xhr.send(opts.body);
171
+ });
172
+ }
173
+ async function openSSEStream(opts) {
174
+ if (!supportsStreamingFetch()) {
175
+ return xhrStream(opts);
176
+ }
177
+ const controller = new AbortController();
178
+ if (opts.signal) {
179
+ if (opts.signal.aborted) controller.abort();
180
+ else opts.signal.addEventListener("abort", () => controller.abort());
181
+ }
182
+ const response = await fetch(opts.url, {
183
+ method: "POST",
184
+ headers: opts.headers,
185
+ body: opts.body,
186
+ signal: controller.signal
187
+ });
188
+ if (!response.ok) {
189
+ const errorText = await response.text().catch(() => "Stream failed");
190
+ return {
191
+ status: response.status,
192
+ ok: false,
193
+ errorText,
194
+ getHeader: (name) => response.headers.get(name),
195
+ reader: { read: async () => ({ done: true }), cancel: () => controller.abort() }
196
+ };
197
+ }
198
+ if (!response.body) {
199
+ const text = await response.text();
200
+ let handed = false;
201
+ return {
202
+ status: response.status,
203
+ ok: true,
204
+ getHeader: (name) => response.headers.get(name),
205
+ reader: {
206
+ read: async () => handed ? { done: true } : (handed = true, { done: false, value: text }),
207
+ cancel: () => controller.abort()
208
+ }
209
+ };
210
+ }
211
+ return {
212
+ status: response.status,
213
+ ok: true,
214
+ getHeader: (name) => response.headers.get(name),
215
+ reader: fetchReader(response, controller)
216
+ };
217
+ }
218
+
219
+ // src/hooks/useChat.ts
45
220
  function isErrorToolContent(content) {
46
221
  if (typeof content !== "string" || !content.trim().startsWith("{")) return false;
47
222
  try {
@@ -108,7 +283,7 @@ function normalizePendingInterrupt(pending) {
108
283
  return null;
109
284
  }
110
285
  function useChat(options = {}) {
111
- const { defaultAgentId, fetchWithAuth } = usePersonaContext();
286
+ const { defaultAgentId, fetchWithAuth, baseUrl, getAuthToken } = usePersonaContext();
112
287
  const agentId = options.agentId || defaultAgentId;
113
288
  const threadId = options.threadId;
114
289
  const [messages, setMessages] = useState(options.initialMessages || []);
@@ -215,16 +390,25 @@ function useChat(options = {}) {
215
390
  setInterrupt(null);
216
391
  const controller = new AbortController();
217
392
  abortControllerRef.current = controller;
393
+ const finalizeReasoning = () => {
394
+ setMessages(
395
+ (prev) => prev.map(
396
+ (m) => m.role === "reasoning" && m.isStreaming ? { ...m, isStreaming: false } : m
397
+ )
398
+ );
399
+ };
218
400
  try {
219
- const payloadMessages = nextMessages.map((m) => ({
401
+ const payloadMessages = nextMessages.filter((m) => m.role !== "reasoning").map((m) => ({
220
402
  role: m.role === "assistant" ? "assistant" : "user",
221
403
  content: m.content
222
404
  }));
223
405
  const resolvedThreadId = await (overrideOptions?.threadId ?? options.threadId);
224
- const response = await fetchWithAuth("/chat", {
225
- method: "POST",
406
+ const token = getAuthToken ? await getAuthToken() : null;
407
+ const stream = await openSSEStream({
408
+ url: `${baseUrl}/chat`,
226
409
  headers: {
227
- "Content-Type": "application/json"
410
+ "Content-Type": "application/json",
411
+ ...token ? { Authorization: `Bearer ${token}` } : {}
228
412
  },
229
413
  body: JSON.stringify({
230
414
  agentId: targetAgentId,
@@ -234,19 +418,15 @@ function useChat(options = {}) {
234
418
  }),
235
419
  signal: controller.signal
236
420
  });
237
- if (!response.ok) {
238
- const errText = await response.text().catch(() => "Stream failed");
239
- throw new Error(`Chat error (${response.status}): ${errText}`);
240
- }
241
- if (!response.body) {
242
- throw new Error("No response body received for streaming.");
421
+ if (!stream.ok) {
422
+ throw new Error(`Chat error (${stream.status}): ${stream.errorText ?? "Stream failed"}`);
243
423
  }
244
- const reader = response.body.getReader();
245
- const decoder = new TextDecoder("utf-8");
424
+ const reader = stream.reader;
246
425
  let buffer = "";
247
426
  let accumulatedText = "";
248
- let accumulatedReasoning = "";
249
427
  const toolCallsMap = /* @__PURE__ */ new Map();
428
+ let activeReasoningId = null;
429
+ const reasoningById = /* @__PURE__ */ new Map();
250
430
  const patchAssistant = (patch) => {
251
431
  setMessages(
252
432
  (prev) => prev.map(
@@ -254,10 +434,26 @@ function useChat(options = {}) {
254
434
  )
255
435
  );
256
436
  };
437
+ const insertReasoningMessage = (id) => {
438
+ const msg = {
439
+ id,
440
+ role: "reasoning",
441
+ content: "",
442
+ createdAt: /* @__PURE__ */ new Date(),
443
+ isStreaming: true
444
+ };
445
+ setMessages((prev) => {
446
+ const idx = prev.findIndex((m) => m.id === assistantMessageId);
447
+ if (idx === -1) return [...prev, msg];
448
+ const next = [...prev];
449
+ next.splice(idx, 0, msg);
450
+ return next;
451
+ });
452
+ };
257
453
  while (true) {
258
454
  const { done, value } = await reader.read();
259
455
  if (done) break;
260
- buffer += decoder.decode(value, { stream: true });
456
+ buffer += value ?? "";
261
457
  const lines = buffer.split("\n");
262
458
  buffer = lines.pop() ?? "";
263
459
  for (const line of lines) {
@@ -297,11 +493,33 @@ function useChat(options = {}) {
297
493
  } else if (event.type === "STATE_SNAPSHOT") {
298
494
  setFiles(normalizeWorkspaceFiles(event.snapshot.files));
299
495
  setTodos(event.snapshot.todos);
496
+ } else if (event.type === "REASONING_MESSAGE_START" && event.messageId) {
497
+ activeReasoningId = event.messageId;
498
+ reasoningById.set(event.messageId, { content: "" });
499
+ insertReasoningMessage(event.messageId);
300
500
  } else if (event.type === "REASONING_MESSAGE_CONTENT") {
301
- accumulatedReasoning += event.delta;
302
- patchAssistant({ reasoning: accumulatedReasoning, isReasoning: true });
501
+ let rid = event.messageId || activeReasoningId || "";
502
+ if (!rid || !reasoningById.has(rid)) {
503
+ rid = rid || `reasoning-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
504
+ activeReasoningId = rid;
505
+ reasoningById.set(rid, { content: "" });
506
+ insertReasoningMessage(rid);
507
+ }
508
+ const entry = reasoningById.get(rid);
509
+ entry.content += event.delta;
510
+ setMessages(
511
+ (prev) => prev.map(
512
+ (m) => m.id === rid ? { ...m, content: entry.content, isStreaming: true } : m
513
+ )
514
+ );
303
515
  } else if (event.type === "REASONING_END") {
304
- patchAssistant({ isReasoning: false });
516
+ const rid = activeReasoningId;
517
+ activeReasoningId = null;
518
+ if (rid) {
519
+ setMessages(
520
+ (prev) => prev.map((m) => m.id === rid ? { ...m, isStreaming: false } : m)
521
+ );
522
+ }
305
523
  } else if (event.type === "CUSTOM") {
306
524
  if (event.name === "hitl_request") {
307
525
  const value2 = event.value;
@@ -337,8 +555,7 @@ function useChat(options = {}) {
337
555
  content: accumulatedText,
338
556
  createdAt: /* @__PURE__ */ new Date(),
339
557
  isStreaming: false,
340
- toolCalls: Array.from(toolCallsMap.values()),
341
- ...accumulatedReasoning ? { reasoning: accumulatedReasoning, isReasoning: false } : {}
558
+ toolCalls: Array.from(toolCallsMap.values())
342
559
  };
343
560
  setMessages(
344
561
  (prev) => prev.map((msg) => msg.id === assistantMessageId ? finalMessage : msg)
@@ -368,9 +585,10 @@ function useChat(options = {}) {
368
585
  } finally {
369
586
  setIsStreaming(false);
370
587
  abortControllerRef.current = null;
588
+ finalizeReasoning();
371
589
  }
372
590
  },
373
- [agentId, fetchWithAuth, input, isStreaming, messages, options]
591
+ [agentId, baseUrl, getAuthToken, input, isStreaming, messages, options]
374
592
  );
375
593
  const handleInputChange = useCallback((e) => {
376
594
  setInput(e.target.value);
@@ -856,10 +1074,12 @@ function useMcpConnections(options = {}) {
856
1074
  }
857
1075
 
858
1076
  // src/index.ts
859
- var VERSION = "0.3.5";
1077
+ var VERSION = "0.4.0";
860
1078
  export {
861
1079
  PersonaProvider,
862
1080
  VERSION,
1081
+ openSSEStream,
1082
+ supportsStreamingFetch,
863
1083
  useAgents,
864
1084
  useChat,
865
1085
  useConnection,