@personaai/react 0.7.9 → 0.8.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
@@ -330,6 +330,37 @@ function parsePresentedFile(content) {
330
330
  return null;
331
331
  }
332
332
  }
333
+ function parseSandboxCommand(tc) {
334
+ let command = tc.args ?? "";
335
+ try {
336
+ const parsedArgs = JSON.parse(tc.args || "{}");
337
+ if (typeof parsedArgs.command === "string") command = parsedArgs.command;
338
+ } catch {
339
+ }
340
+ let output;
341
+ let exitCode;
342
+ if (tc.result) {
343
+ try {
344
+ const parsedResult = JSON.parse(tc.result);
345
+ if (typeof parsedResult.output === "string") {
346
+ output = parsedResult.output;
347
+ exitCode = typeof parsedResult.exitCode === "number" ? parsedResult.exitCode : null;
348
+ } else {
349
+ output = tc.result;
350
+ }
351
+ } catch {
352
+ output = tc.result;
353
+ }
354
+ }
355
+ return {
356
+ toolCallId: tc.toolCallId,
357
+ command,
358
+ output,
359
+ exitCode,
360
+ status: tc.isError ? "error" : tc.result ? "done" : "running",
361
+ seq: tc.seq
362
+ };
363
+ }
333
364
  function normalizeWorkspaceFiles(raw) {
334
365
  const normalized = {};
335
366
  for (const [path, file] of Object.entries(raw || {})) {
@@ -365,6 +396,11 @@ function useChat(options = {}) {
365
396
  const chatLogger = useMemo2(() => logger.child("chat"), [logger]);
366
397
  const agentId = options.agentId || defaultAgentId;
367
398
  const threadId = options.threadId;
399
+ const [internalThreadId, setInternalThreadId] = useState(void 0);
400
+ const effectiveThreadId = threadId ?? internalThreadId;
401
+ useEffect(() => {
402
+ if (threadId !== void 0) setInternalThreadId(threadId);
403
+ }, [threadId]);
368
404
  const didLogInitRef = useRef(false);
369
405
  if (!didLogInitRef.current) {
370
406
  didLogInitRef.current = true;
@@ -390,11 +426,21 @@ function useChat(options = {}) {
390
426
  const [files, setFiles] = useState({});
391
427
  const [todos, setTodos] = useState([]);
392
428
  const [presentedFile, setPresentedFile] = useState(null);
429
+ const sandboxCommands = useMemo2(() => {
430
+ const commands = [];
431
+ for (const message of messages) {
432
+ for (const tc of message.toolCalls ?? []) {
433
+ if (tc.toolName === "execute") commands.push(parseSandboxCommand(tc));
434
+ }
435
+ }
436
+ return commands;
437
+ }, [messages]);
393
438
  const abortControllerRef = useRef(null);
394
439
  const loadedThreadIdRef = useRef(void 0);
395
- const voiceThreadRef = useRef(threadId);
440
+ const voiceThreadRef = useRef(effectiveThreadId);
396
441
  const voicePrevLenRef = useRef(0);
397
442
  const voiceStreamingIdRef = useRef(null);
443
+ const voiceUserPartialIdRef = useRef(null);
398
444
  const stop = useCallback(() => {
399
445
  if (abortControllerRef.current) {
400
446
  chatLogger.info("stop streaming", {});
@@ -419,7 +465,25 @@ function useChat(options = {}) {
419
465
  setFiles({});
420
466
  setTodos([]);
421
467
  setPresentedFile(null);
468
+ loadedThreadIdRef.current = void 0;
422
469
  }, [stop, chatLogger, messages.length]);
470
+ const startNewChat = useCallback(() => {
471
+ chatLogger.info("startNewChat \u2014 entering ephemeral mode");
472
+ stop();
473
+ setMessages([]);
474
+ setError(null);
475
+ setInterrupt(null);
476
+ setFiles({});
477
+ setTodos([]);
478
+ setPresentedFile(null);
479
+ setInput("");
480
+ loadedThreadIdRef.current = void 0;
481
+ voiceThreadRef.current = void 0;
482
+ voicePrevLenRef.current = 0;
483
+ voiceStreamingIdRef.current = null;
484
+ voiceUserPartialIdRef.current = null;
485
+ setInternalThreadId(void 0);
486
+ }, [stop, chatLogger]);
423
487
  const loadThreadMessages = useCallback(
424
488
  async (id) => {
425
489
  chatLogger.debug("loadThreadMessages start", { threadId: id });
@@ -451,6 +515,7 @@ function useChat(options = {}) {
451
515
  setInterrupt(normalizePendingInterrupt(data?.pendingInterrupt));
452
516
  setFiles(normalizeWorkspaceFiles(data?.state?.files ?? {}));
453
517
  setTodos(data?.state?.todos ?? []);
518
+ loadedThreadIdRef.current = id;
454
519
  chatLogger.info("loadThreadMessages succeeded", {
455
520
  threadId: id,
456
521
  messageCount: loaded.length
@@ -471,6 +536,7 @@ function useChat(options = {}) {
471
536
  error: errorObj.message
472
537
  });
473
538
  setError(errorObj);
539
+ if (loadedThreadIdRef.current === id) loadedThreadIdRef.current = void 0;
474
540
  return [];
475
541
  } finally {
476
542
  setIsLoadingHistory(false);
@@ -480,33 +546,33 @@ function useChat(options = {}) {
480
546
  [fetchWithAuth, chatLogger]
481
547
  );
482
548
  useEffect(() => {
483
- if (!threadId || isStreaming) {
484
- chatLogger.trace("auto-load skipped", { threadId, isStreaming });
549
+ if (!effectiveThreadId || isStreaming) {
550
+ chatLogger.trace("auto-load skipped", { threadId: effectiveThreadId, isStreaming });
485
551
  return;
486
552
  }
487
- if (loadedThreadIdRef.current === threadId) {
488
- chatLogger.trace("auto-load already loaded", { threadId });
553
+ if (loadedThreadIdRef.current === effectiveThreadId) {
554
+ chatLogger.trace("auto-load already loaded", { threadId: effectiveThreadId });
489
555
  return;
490
556
  }
491
557
  if (messages.length > 0) {
492
558
  chatLogger.trace("auto-load has messages", {
493
- threadId,
559
+ threadId: effectiveThreadId,
494
560
  count: messages.length
495
561
  });
496
562
  return;
497
563
  }
498
- loadedThreadIdRef.current = threadId;
499
- chatLogger.info("auto-load thread history", { threadId });
500
- chatLogger.debug("loadThreadMessages trigger", { threadId });
501
- void loadThreadMessages(threadId);
502
- }, [threadId, isStreaming, messages.length, loadThreadMessages, chatLogger]);
564
+ chatLogger.info("auto-load thread history", { threadId: effectiveThreadId });
565
+ chatLogger.debug("loadThreadMessages trigger", { threadId: effectiveThreadId });
566
+ void loadThreadMessages(effectiveThreadId);
567
+ }, [effectiveThreadId, isStreaming, messages.length, loadThreadMessages, chatLogger]);
503
568
  const voice = options.voice;
504
569
  useEffect(() => {
505
570
  if (!voice) return;
506
- if (voiceThreadRef.current !== threadId) {
507
- voiceThreadRef.current = threadId;
571
+ if (voiceThreadRef.current !== effectiveThreadId) {
572
+ voiceThreadRef.current = effectiveThreadId;
508
573
  voicePrevLenRef.current = voice.transcript.length;
509
574
  voiceStreamingIdRef.current = null;
575
+ voiceUserPartialIdRef.current = null;
510
576
  return;
511
577
  }
512
578
  const isVoiceActive = voice.state !== "idle" && voice.state !== "ended" && voice.state !== "error";
@@ -514,15 +580,40 @@ function useChat(options = {}) {
514
580
  if (curLen < voicePrevLenRef.current) {
515
581
  voicePrevLenRef.current = 0;
516
582
  voiceStreamingIdRef.current = null;
583
+ voiceUserPartialIdRef.current = null;
517
584
  }
518
585
  if (!isVoiceActive) {
519
- voicePrevLenRef.current = voice.transcript.length;
586
+ if (curLen > voicePrevLenRef.current) {
587
+ const pendingLines = voice.transcript.slice(voicePrevLenRef.current);
588
+ voicePrevLenRef.current = curLen;
589
+ for (const line of pendingLines) {
590
+ const text = (line.text || "").trim();
591
+ if (!text) continue;
592
+ const role = line.speaker === "user" ? "user" : "assistant";
593
+ const voiceId = `voice-${line.id}`;
594
+ setMessages((prev) => {
595
+ if (prev.some((m) => m.id === voiceId)) return prev;
596
+ if (prev.some((m) => !m.id.startsWith("voice-") && m.role === role && m.content.trim() === text)) return prev;
597
+ const last = prev[prev.length - 1];
598
+ if (last?.id.startsWith("voice-") && last.role === "assistant" && role === "assistant") {
599
+ const merged = text.startsWith(last.content) ? text : `${last.content} ${text}`.trim();
600
+ return [...prev.slice(0, -1), { ...last, content: merged, isStreaming: false }];
601
+ }
602
+ return [...prev, { id: voiceId, role, content: text, createdAt: /* @__PURE__ */ new Date() }];
603
+ });
604
+ }
605
+ } else {
606
+ voicePrevLenRef.current = voice.transcript.length;
607
+ }
520
608
  if (voiceStreamingIdRef.current) {
521
609
  const doneId = voiceStreamingIdRef.current;
522
610
  voiceStreamingIdRef.current = null;
523
- setMessages(
524
- (prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m)
525
- );
611
+ setMessages((prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m));
612
+ }
613
+ if (voiceUserPartialIdRef.current) {
614
+ const doneId = voiceUserPartialIdRef.current;
615
+ voiceUserPartialIdRef.current = null;
616
+ setMessages((prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m));
526
617
  }
527
618
  return;
528
619
  }
@@ -549,54 +640,71 @@ function useChat(options = {}) {
549
640
  });
550
641
  }
551
642
  }
552
- const partialText = voice.partial?.speaker === "agent" ? voice.partial.text.trim() : "";
553
- if (partialText) {
643
+ const agentPartialText = voice.partial?.speaker === "agent" ? voice.partial.text.trim() : "";
644
+ const userPartialText = voice.partial?.speaker === "user" ? voice.partial.text.trim() : "";
645
+ if (agentPartialText) {
646
+ if (voiceUserPartialIdRef.current) {
647
+ const doneId = voiceUserPartialIdRef.current;
648
+ voiceUserPartialIdRef.current = null;
649
+ setMessages((prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m));
650
+ }
554
651
  setMessages((prev) => {
555
652
  const last = prev[prev.length - 1];
556
653
  if (last?.id.startsWith("voice-") && last.role === "assistant") {
557
654
  voiceStreamingIdRef.current = last.id;
558
- return [...prev.slice(0, -1), { ...last, content: partialText, isStreaming: true }];
655
+ return [...prev.slice(0, -1), { ...last, content: agentPartialText, isStreaming: true }];
559
656
  }
560
657
  const id = voiceStreamingIdRef.current || `voice-partial-${Date.now()}`;
561
658
  voiceStreamingIdRef.current = id;
562
- return [...prev, { id, role: "assistant", content: partialText, isStreaming: true, createdAt: /* @__PURE__ */ new Date() }];
659
+ return [...prev, { id, role: "assistant", content: agentPartialText, isStreaming: true, createdAt: /* @__PURE__ */ new Date() }];
563
660
  });
564
- } else if (voice.state !== "speaking" && voiceStreamingIdRef.current) {
565
- const doneId = voiceStreamingIdRef.current;
566
- voiceStreamingIdRef.current = null;
567
- setMessages(
568
- (prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m)
569
- );
570
- }
571
- }, [voice?.transcript, voice?.partial, voice?.state, threadId]);
572
- const sendMessage = useCallback(
573
- async (contentToSend, overrideOptions) => {
574
- const prompt = (contentToSend ?? input).trim();
575
- if (!prompt || isStreaming) {
576
- chatLogger.trace("sendMessage skipped", {
577
- hasPrompt: !!prompt,
578
- isStreaming
579
- });
580
- return;
661
+ } else if (userPartialText) {
662
+ if (voiceStreamingIdRef.current) {
663
+ const doneId = voiceStreamingIdRef.current;
664
+ voiceStreamingIdRef.current = null;
665
+ setMessages((prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m));
581
666
  }
667
+ setMessages((prev) => {
668
+ const last = prev[prev.length - 1];
669
+ if (last?.id.startsWith("voice-") && last.role === "user" && last.isStreaming) {
670
+ voiceUserPartialIdRef.current = last.id;
671
+ return [...prev.slice(0, -1), { ...last, content: userPartialText, isStreaming: true }];
672
+ }
673
+ const id = voiceUserPartialIdRef.current || `voice-partial-user-${Date.now()}`;
674
+ voiceUserPartialIdRef.current = id;
675
+ return [...prev, { id, role: "user", content: userPartialText, isStreaming: true, createdAt: /* @__PURE__ */ new Date() }];
676
+ });
677
+ } else {
678
+ if (voiceStreamingIdRef.current && voice.state !== "speaking") {
679
+ const doneId = voiceStreamingIdRef.current;
680
+ voiceStreamingIdRef.current = null;
681
+ setMessages((prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m));
682
+ }
683
+ if (voiceUserPartialIdRef.current) {
684
+ const doneId = voiceUserPartialIdRef.current;
685
+ voiceUserPartialIdRef.current = null;
686
+ setMessages((prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m));
687
+ }
688
+ }
689
+ }, [voice?.transcript, voice?.partial, voice?.state, effectiveThreadId]);
690
+ const doSend = useCallback(
691
+ async (prompt, baseMessages, overrideOptions) => {
582
692
  const targetAgentId = overrideOptions?.agentId || agentId;
583
693
  if (!targetAgentId) {
584
694
  const err = new Error("No Agent ID specified for useChat.");
585
695
  chatLogger.warn("sendMessage no agentId", {});
586
- chatLogger.error("sendMessage failed \u2014 no agent", {
587
- error: err.message
588
- });
696
+ chatLogger.error("sendMessage failed \u2014 no agent", { error: err.message });
589
697
  setError(err);
590
698
  options.onError?.(err);
591
- return;
699
+ return false;
592
700
  }
593
701
  chatLogger.debug("sendMessage start", {
594
702
  promptPreview: prompt.slice(0, 100),
595
703
  agentId: targetAgentId,
596
- threadId: threadId ?? overrideOptions?.threadId,
704
+ threadId: effectiveThreadId ?? overrideOptions?.threadId,
597
705
  hasResume: !!overrideOptions?.resume,
598
706
  hasContextOverride: !!overrideOptions?.contextOverride,
599
- messageCount: messages.length
707
+ messageCount: baseMessages.length
600
708
  });
601
709
  chatLogger.trace("sendMessage details", {
602
710
  agentId: targetAgentId,
@@ -619,7 +727,7 @@ function useChat(options = {}) {
619
727
  isStreaming: true,
620
728
  toolCalls: []
621
729
  };
622
- const nextMessages = [...messages, userMessage];
730
+ const nextMessages = [...baseMessages, userMessage];
623
731
  setMessages([...nextMessages, placeholderAssistant]);
624
732
  setInput("");
625
733
  setIsStreaming(true);
@@ -629,9 +737,7 @@ function useChat(options = {}) {
629
737
  abortControllerRef.current = controller;
630
738
  const finalizeReasoning = () => {
631
739
  setMessages(
632
- (prev) => prev.map(
633
- (m) => m.role === "reasoning" && m.isStreaming ? { ...m, isStreaming: false } : m
634
- )
740
+ (prev) => prev.map((m) => m.role === "reasoning" && m.isStreaming ? { ...m, isStreaming: false } : m)
635
741
  );
636
742
  };
637
743
  try {
@@ -639,7 +745,24 @@ function useChat(options = {}) {
639
745
  role: m.role === "assistant" ? "assistant" : "user",
640
746
  content: m.content
641
747
  }));
642
- const resolvedThreadId = await (overrideOptions?.threadId ?? options.threadId);
748
+ let resolvedThreadId = await overrideOptions?.threadId ?? effectiveThreadId;
749
+ if (!resolvedThreadId) {
750
+ chatLogger.info("minting real thread for ephemeral chat", { agentId: targetAgentId });
751
+ const createRes = await fetchWithAuth("/threads", {
752
+ method: "POST",
753
+ headers: { "Content-Type": "application/json" },
754
+ body: JSON.stringify({ agentId: targetAgentId })
755
+ });
756
+ if (!createRes.ok) throw new Error(`Failed to create thread: ${createRes.statusText}`);
757
+ const createBody = await createRes.json();
758
+ const created = createBody?.data ?? createBody;
759
+ resolvedThreadId = created._id ?? created.id;
760
+ if (!resolvedThreadId) throw new Error("Thread creation returned no id");
761
+ setInternalThreadId(resolvedThreadId);
762
+ loadedThreadIdRef.current = resolvedThreadId;
763
+ options.onThreadCreated?.(resolvedThreadId);
764
+ chatLogger.info("ephemeral chat minted", { threadId: resolvedThreadId });
765
+ }
643
766
  chatLogger.trace("resolved threadId", { threadId: resolvedThreadId });
644
767
  const token = getAuthToken ? await getAuthToken() : null;
645
768
  const resolvedHookContext = typeof options.context === "function" ? options.context() : options.context;
@@ -668,27 +791,12 @@ function useChat(options = {}) {
668
791
  signal: controller.signal
669
792
  });
670
793
  if (!stream.ok) {
671
- chatLogger.warn("SSE stream not ok", {
672
- status: stream.status,
673
- errorText: stream.errorText
674
- });
675
- chatLogger.error("chat stream failed", {
676
- agentId: targetAgentId,
677
- status: stream.status,
678
- error: stream.errorText
679
- });
680
- throw new Error(
681
- `Chat error (${stream.status}): ${stream.errorText ?? "Stream failed"}`
682
- );
794
+ chatLogger.warn("SSE stream not ok", { status: stream.status, errorText: stream.errorText });
795
+ chatLogger.error("chat stream failed", { agentId: targetAgentId, status: stream.status, error: stream.errorText });
796
+ throw new Error(`Chat error (${stream.status}): ${stream.errorText ?? "Stream failed"}`);
683
797
  }
684
- chatLogger.debug("SSE stream opened", {
685
- agentId: targetAgentId,
686
- status: stream.status
687
- });
688
- chatLogger.info("chat stream started", {
689
- agentId: targetAgentId,
690
- threadId: resolvedThreadId
691
- });
798
+ chatLogger.debug("SSE stream opened", { agentId: targetAgentId, status: stream.status });
799
+ chatLogger.info("chat stream started", { agentId: targetAgentId, threadId: resolvedThreadId });
692
800
  const reader = stream.reader;
693
801
  let buffer = "";
694
802
  let accumulatedText = "";
@@ -699,23 +807,12 @@ function useChat(options = {}) {
699
807
  const patchAssistant = (patch) => {
700
808
  setMessages(
701
809
  (prev) => prev.map(
702
- (msg) => msg.id === assistantMessageId ? {
703
- ...msg,
704
- toolCalls: Array.from(toolCallsMap.values()),
705
- ...patch
706
- } : msg
810
+ (msg) => msg.id === assistantMessageId ? { ...msg, toolCalls: Array.from(toolCallsMap.values()), ...patch } : msg
707
811
  )
708
812
  );
709
813
  };
710
814
  const insertReasoningMessage = (id, seq) => {
711
- const msg = {
712
- id,
713
- role: "reasoning",
714
- content: "",
715
- createdAt: /* @__PURE__ */ new Date(),
716
- isStreaming: true,
717
- seq
718
- };
815
+ const msg = { id, role: "reasoning", content: "", createdAt: /* @__PURE__ */ new Date(), isStreaming: true, seq };
719
816
  setMessages((prev) => {
720
817
  const idx = prev.findIndex((m) => m.id === assistantMessageId);
721
818
  if (idx === -1) return [...prev, msg];
@@ -740,16 +837,11 @@ function useChat(options = {}) {
740
837
  chatLogger.trace("stream event", { type: event.type, event });
741
838
  options.onEvent?.(event);
742
839
  if (event.type === "TEXT_MESSAGE_CHUNK" && event.delta) {
743
- chatLogger.debug("text chunk", {
744
- deltaLength: event.delta.length
745
- });
840
+ chatLogger.debug("text chunk", { deltaLength: event.delta.length });
746
841
  accumulatedText += event.delta;
747
842
  patchAssistant({ content: accumulatedText, isStreaming: true });
748
843
  } else if (event.type === "TOOL_CALL_CHUNK" && event.toolCallId) {
749
- chatLogger.debug("tool call chunk", {
750
- toolCallId: event.toolCallId,
751
- toolCallName: event.toolCallName
752
- });
844
+ chatLogger.debug("tool call chunk", { toolCallId: event.toolCallId, toolCallName: event.toolCallName });
753
845
  const existing = toolCallsMap.get(event.toolCallId);
754
846
  if (existing) {
755
847
  existing.args = (existing.args || "") + (event.delta || "");
@@ -763,17 +855,12 @@ function useChat(options = {}) {
763
855
  }
764
856
  patchAssistant({});
765
857
  } else if (event.type === "TOOL_CALL_RESULT") {
766
- chatLogger.debug("tool call result", {
767
- toolCallId: event.toolCallId
768
- });
858
+ chatLogger.debug("tool call result", { toolCallId: event.toolCallId });
769
859
  const existing = toolCallsMap.get(event.toolCallId);
770
860
  if (existing) {
771
861
  existing.result = event.content;
772
862
  existing.isError = isErrorToolContent(event.content);
773
- if (existing.isError)
774
- chatLogger.warn("tool call error", {
775
- toolCallId: event.toolCallId
776
- });
863
+ if (existing.isError) chatLogger.warn("tool call error", { toolCallId: event.toolCallId });
777
864
  if (existing.toolName === "present_file" && !existing.isError) {
778
865
  const presented = parsePresentedFile(event.content);
779
866
  if (presented) {
@@ -788,12 +875,10 @@ function useChat(options = {}) {
788
875
  fileCount: Object.keys(event.snapshot.files ?? {}).length,
789
876
  todoCount: event.snapshot.todos?.length ?? 0
790
877
  });
791
- setFiles(normalizeWorkspaceFiles(event.snapshot.files));
792
- setTodos(event.snapshot.todos);
878
+ setFiles(normalizeWorkspaceFiles(event.snapshot.files ?? {}));
879
+ setTodos(event.snapshot.todos ?? []);
793
880
  } else if (event.type === "REASONING_MESSAGE_START" && event.messageId) {
794
- chatLogger.debug("reasoning start", {
795
- messageId: event.messageId
796
- });
881
+ chatLogger.debug("reasoning start", { messageId: event.messageId });
797
882
  activeReasoningId = event.messageId;
798
883
  reasoningById.set(event.messageId, { content: "" });
799
884
  insertReasoningMessage(event.messageId, streamSeq++);
@@ -808,61 +893,31 @@ function useChat(options = {}) {
808
893
  }
809
894
  const entry = reasoningById.get(rid);
810
895
  entry.content += event.delta;
811
- chatLogger.trace("reasoning content", {
812
- messageId: rid,
813
- deltaLength: event.delta?.length ?? 0
814
- });
815
- setMessages(
816
- (prev) => prev.map(
817
- (m) => m.id === rid ? { ...m, content: entry.content, isStreaming: true } : m
818
- )
819
- );
896
+ chatLogger.trace("reasoning content", { messageId: rid, deltaLength: event.delta?.length ?? 0 });
897
+ setMessages((prev) => prev.map((m) => m.id === rid ? { ...m, content: entry.content, isStreaming: true } : m));
820
898
  } else if (event.type === "REASONING_END") {
821
- chatLogger.debug("reasoning end", {
822
- messageId: activeReasoningId
823
- });
899
+ chatLogger.debug("reasoning end", { messageId: activeReasoningId });
824
900
  const rid = activeReasoningId;
825
901
  activeReasoningId = null;
826
902
  if (rid) {
827
- setMessages(
828
- (prev) => prev.map(
829
- (m) => m.id === rid ? { ...m, isStreaming: false } : m
830
- )
831
- );
903
+ setMessages((prev) => prev.map((m) => m.id === rid ? { ...m, isStreaming: false } : m));
832
904
  }
833
905
  } else if (event.type === "CUSTOM") {
834
906
  chatLogger.debug("custom event", { name: event.name });
835
907
  if (event.name === "hitl_request") {
836
908
  const value2 = event.value;
837
- chatLogger.info("hitl interrupt", {
838
- actionCount: value2.actionRequests?.length ?? 0
839
- });
840
- setInterrupt({
841
- kind: "hitl",
842
- actionRequests: value2.actionRequests,
843
- reviewConfigs: value2.reviewConfigs
844
- });
909
+ chatLogger.info("hitl interrupt", { actionCount: value2.actionRequests?.length ?? 0 });
910
+ setInterrupt({ kind: "hitl", actionRequests: value2.actionRequests, reviewConfigs: value2.reviewConfigs });
845
911
  } else if (event.name === "clarification_request") {
846
912
  const value2 = event.value;
847
- chatLogger.info("clarification interrupt", {
848
- questionCount: value2.questions?.length ?? 0
849
- });
850
- setInterrupt({
851
- kind: "clarification",
852
- questions: value2.questions
853
- });
913
+ chatLogger.info("clarification interrupt", { questionCount: value2.questions?.length ?? 0 });
914
+ setInterrupt({ kind: "clarification", questions: value2.questions });
854
915
  } else if (event.name === "subagent_activity") {
855
916
  const { toolCallId, ...entry } = event.value;
856
- chatLogger.trace("subagent activity", {
857
- toolCallId,
858
- kind: entry.kind
859
- });
917
+ chatLogger.trace("subagent activity", { toolCallId, kind: entry.kind });
860
918
  const existing = toolCallsMap.get(toolCallId);
861
919
  if (existing) {
862
- existing.subagentActivity = [
863
- ...existing.subagentActivity || [],
864
- entry
865
- ];
920
+ existing.subagentActivity = [...existing.subagentActivity || [], entry];
866
921
  patchAssistant({});
867
922
  }
868
923
  } else if (event.name === "mcp_app") {
@@ -870,35 +925,21 @@ function useChat(options = {}) {
870
925
  if (val?.toolCallId && val?.resourceUri && val?.mcpId) {
871
926
  const existing = toolCallsMap.get(val.toolCallId);
872
927
  if (existing) {
873
- existing.mcpApp = {
874
- resourceUri: val.resourceUri,
875
- mcpId: val.mcpId
876
- };
928
+ existing.mcpApp = { resourceUri: val.resourceUri, mcpId: val.mcpId };
877
929
  patchAssistant({});
878
930
  }
879
931
  }
880
932
  }
881
933
  } else if (event.type === "RUN_ERROR") {
882
- chatLogger.warn("run error", {
883
- message: event.message,
884
- code: event.code
885
- });
886
- chatLogger.error("stream run error", {
887
- code: event.code,
888
- message: event.message
889
- });
934
+ chatLogger.warn("run error", { message: event.message, code: event.code });
935
+ chatLogger.error("stream run error", { code: event.code, message: event.message });
890
936
  throw new Error(event.message || "Stream error from agent");
891
937
  } else {
892
938
  chatLogger.trace("unhandled event", { type: event.type });
893
939
  }
894
940
  } catch (e) {
895
- if (e instanceof Error && e.message.startsWith("Stream error")) {
896
- throw e;
897
- }
898
- chatLogger.warn("event parse error", {
899
- raw: raw.slice(0, 200),
900
- error: e instanceof Error ? e.message : String(e)
901
- });
941
+ if (e instanceof Error && e.message.startsWith("Stream error")) throw e;
942
+ chatLogger.warn("event parse error", { raw: raw.slice(0, 200), error: e instanceof Error ? e.message : String(e) });
902
943
  }
903
944
  }
904
945
  }
@@ -910,75 +951,55 @@ function useChat(options = {}) {
910
951
  isStreaming: false,
911
952
  toolCalls: Array.from(toolCallsMap.values())
912
953
  };
913
- setMessages(
914
- (prev) => prev.map(
915
- (msg) => msg.id === assistantMessageId ? finalMessage : msg
916
- )
917
- );
918
- chatLogger.info("sendMessage succeeded", {
919
- agentId: targetAgentId,
920
- textLength: accumulatedText.length,
921
- toolCallCount: toolCallsMap.size
922
- });
923
- chatLogger.debug("sendMessage completed", {
924
- agentId: targetAgentId,
925
- textLength: accumulatedText.length,
926
- eventCount: toolCallsMap.size + 1
927
- });
954
+ setMessages((prev) => prev.map((msg) => msg.id === assistantMessageId ? finalMessage : msg));
955
+ chatLogger.info("sendMessage succeeded", { agentId: targetAgentId, textLength: accumulatedText.length, toolCallCount: toolCallsMap.size });
956
+ chatLogger.debug("sendMessage completed", { agentId: targetAgentId, textLength: accumulatedText.length, eventCount: toolCallsMap.size + 1 });
928
957
  options.onFinish?.(finalMessage);
958
+ return true;
929
959
  } catch (err) {
930
960
  if (controller.signal.aborted) {
931
961
  chatLogger.info("sendMessage aborted", { agentId: targetAgentId });
932
962
  chatLogger.debug("stream aborted", { agentId: targetAgentId });
933
- setMessages(
934
- (prev) => prev.map(
935
- (msg) => msg.id === assistantMessageId ? { ...msg, isStreaming: false } : msg
936
- )
937
- );
938
- return;
963
+ setMessages((prev) => prev.map((msg) => msg.id === assistantMessageId ? { ...msg, isStreaming: false } : msg));
964
+ return false;
939
965
  }
940
966
  const errorObj = err instanceof Error ? err : new Error(String(err));
941
- chatLogger.warn("sendMessage failed", {
942
- agentId: targetAgentId,
943
- error: errorObj.message
944
- });
945
- chatLogger.error("sendMessage error", {
946
- agentId: targetAgentId,
947
- error: errorObj.message
948
- });
967
+ chatLogger.warn("sendMessage failed", { agentId: targetAgentId, error: errorObj.message });
968
+ chatLogger.error("sendMessage error", { agentId: targetAgentId, error: errorObj.message });
949
969
  setError(errorObj);
950
970
  options.onError?.(errorObj);
951
971
  setMessages(
952
972
  (prev) => prev.map(
953
- (msg) => msg.id === assistantMessageId ? {
954
- ...msg,
955
- content: msg.content || `\u26A0\uFE0F Error: ${errorObj.message || "Failed to get response."}`,
956
- isStreaming: false
957
- } : msg
973
+ (msg) => msg.id === assistantMessageId ? { ...msg, content: msg.content || `\u26A0\uFE0F Error: ${errorObj.message || "Failed to get response."}`, isStreaming: false } : msg
958
974
  )
959
975
  );
976
+ return false;
960
977
  } finally {
961
978
  setIsStreaming(false);
962
979
  abortControllerRef.current = null;
963
980
  finalizeReasoning();
964
- chatLogger.debug("sendMessage finally", {
965
- agentId: targetAgentId,
966
- isStreaming: false
967
- });
981
+ chatLogger.debug("sendMessage finally", { agentId: targetAgentId, isStreaming: false });
968
982
  chatLogger.trace("sendMessage end", { agentId: targetAgentId });
969
983
  }
970
984
  },
971
- [
972
- agentId,
973
- baseUrl,
974
- getAuthToken,
975
- input,
976
- isStreaming,
977
- messages,
978
- options,
979
- chatLogger,
980
- threadId
981
- ]
985
+ [agentId, baseUrl, getAuthToken, fetchWithAuth, options, chatLogger, effectiveThreadId]
986
+ );
987
+ const sendMessage = useCallback(
988
+ async (contentToSend, overrideOptions) => {
989
+ const prompt = (contentToSend ?? input).trim();
990
+ if (!prompt || isStreaming) {
991
+ chatLogger.trace("sendMessage skipped", {
992
+ hasPrompt: !!prompt,
993
+ isStreaming
994
+ });
995
+ if (isStreaming) {
996
+ chatLogger.warn("sendMessage dropped while streaming \u2014 caller should queue or disable send", {});
997
+ }
998
+ return false;
999
+ }
1000
+ return doSend(prompt, messages, overrideOptions);
1001
+ },
1002
+ [input, isStreaming, messages, doSend, chatLogger]
982
1003
  );
983
1004
  const handleInputChange = useCallback(
984
1005
  (e) => {
@@ -995,13 +1016,13 @@ function useChat(options = {}) {
995
1016
  },
996
1017
  [sendMessage, chatLogger]
997
1018
  );
998
- const reload = useCallback(() => {
1019
+ const reload = useCallback(async () => {
999
1020
  if (messages.length === 0 || isStreaming) {
1000
1021
  chatLogger.trace("reload skipped", {
1001
1022
  messageCount: messages.length,
1002
1023
  isStreaming
1003
1024
  });
1004
- return;
1025
+ return false;
1005
1026
  }
1006
1027
  let lastUserIndex = -1;
1007
1028
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -1012,16 +1033,16 @@ function useChat(options = {}) {
1012
1033
  }
1013
1034
  if (lastUserIndex === -1) {
1014
1035
  chatLogger.warn("reload no user message", {});
1015
- return;
1036
+ return false;
1016
1037
  }
1017
1038
  const lastUserMessage = messages[lastUserIndex];
1039
+ const truncated = messages.slice(0, lastUserIndex);
1018
1040
  chatLogger.info("reload", { messageId: lastUserMessage.id });
1019
1041
  chatLogger.debug("reload last user", {
1020
1042
  preview: lastUserMessage.content.slice(0, 100)
1021
1043
  });
1022
- setMessages(messages.slice(0, lastUserIndex));
1023
- void sendMessage(lastUserMessage.content);
1024
- }, [isStreaming, messages, sendMessage, chatLogger]);
1044
+ return doSend(lastUserMessage.content, truncated);
1045
+ }, [isStreaming, messages, doSend, chatLogger]);
1025
1046
  const resumeInterrupt = useCallback(
1026
1047
  (resume, displayContent) => {
1027
1048
  chatLogger.info("resumeInterrupt", {
@@ -1065,9 +1086,13 @@ function useChat(options = {}) {
1065
1086
  presentedFile,
1066
1087
  dismissPresentedFile,
1067
1088
  openWorkspaceFile,
1089
+ sandboxCommands,
1068
1090
  stop,
1069
1091
  reload,
1070
1092
  clear,
1093
+ startNewChat,
1094
+ currentThreadId: effectiveThreadId,
1095
+ isEphemeral: !effectiveThreadId,
1071
1096
  setMessages,
1072
1097
  loadThreadMessages
1073
1098
  };
@@ -2036,7 +2061,7 @@ import {
2036
2061
  getLogLevel,
2037
2062
  isLevelEnabled
2038
2063
  } from "@personaai/logger";
2039
- var VERSION = "0.7.9";
2064
+ var VERSION = "0.8.0";
2040
2065
  export {
2041
2066
  PersonaProvider,
2042
2067
  VERSION,