@agentforge-io/chat-sdk 2.4.0-dev.11 → 2.4.0-dev.13

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.
Files changed (2) hide show
  1. package/dist/react.js +34 -5
  2. package/package.json +1 -1
package/dist/react.js CHANGED
@@ -291,14 +291,31 @@ function ChatWidget(props) {
291
291
  el.focus({ preventScroll: true });
292
292
  focusedOnceRef.current = true;
293
293
  }, [status]);
294
- // ── Session lifecycle. Recreate when token / apiBaseUrl changes. ──────
294
+ // ── Session lifecycle. Recreate when token / apiBaseUrl / browserSessionId
295
+ // / stream changes. NOTE: `resumeConversationId` is intentionally NOT a
296
+ // dependency.
297
+ //
298
+ // Why: hosts typically wire `resumeConversationId={conversationId}`
299
+ // and call `setConversationId(id)` in `onConversationStart`. The
300
+ // conversation_started event fires AFTER the SSE stream opens and
301
+ // the first chunks already started flowing. If we treated
302
+ // resumeConversationId as a reactive dep, this effect would
303
+ // re-run mid-stream, destroy the live ChatSession, and the
304
+ // remaining chunks would land in a corpse — the visitor sees
305
+ // an empty assistant bubble even though the backend completed
306
+ // the turn successfully.
307
+ //
308
+ // We capture the resume id at mount via a ref. Subsequent host
309
+ // updates to it are ignored — the SDK already owns the live
310
+ // conversation id internally via session.state.conversationId.
311
+ const resumeRef = (0, react_1.useRef)(resumeConversationId);
295
312
  (0, react_1.useEffect)(() => {
296
313
  let cancelled = false;
297
314
  const s = new session_1.ChatSession({
298
315
  token,
299
316
  apiBaseUrl,
300
317
  browserSessionId,
301
- resumeConversationId,
318
+ resumeConversationId: resumeRef.current,
302
319
  stream,
303
320
  });
304
321
  setSession(s);
@@ -336,7 +353,10 @@ function ChatWidget(props) {
336
353
  unsubscribe();
337
354
  s.destroy();
338
355
  };
339
- }, [token, apiBaseUrl, browserSessionId, resumeConversationId, stream]);
356
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- resumeConversationId
357
+ // is captured via resumeRef on first mount; reactive updates are
358
+ // ignored by design. See block comment above.
359
+ }, [token, apiBaseUrl, browserSessionId, stream]);
340
360
  // Auto-scroll on new tokens.
341
361
  //
342
362
  // We defer the scroll into a requestAnimationFrame so the DOM has
@@ -546,7 +566,9 @@ function ChatWidget(props) {
546
566
  onShortcutClick(text, i);
547
567
  else
548
568
  setDraft(text);
549
- }, children: text }, `${i}-${text}`))) })), (0, jsx_runtime_1.jsxs)("div", { className: "af-input-row", children: [composerLeftSlot && ((0, jsx_runtime_1.jsx)("div", { className: "af-input-left", children: composerLeftSlot })), (0, jsx_runtime_1.jsx)("textarea", { ref: inputRef, className: "af-input", value: draft, onChange: (e) => setDraft(e.target.value), onKeyDown: onKeyDown, placeholder: inputPlaceholder ?? 'Type a message…', rows: 1,
569
+ }, children: text }, `${i}-${text}`))) })), (0, jsx_runtime_1.jsxs)("div", { className: "af-input-row", "data-loading": status === 'loading' || status === 'idle' ? '' : undefined, children: [composerLeftSlot && ((0, jsx_runtime_1.jsx)("div", { className: "af-input-left", children: composerLeftSlot })), (0, jsx_runtime_1.jsx)("textarea", { ref: inputRef, className: "af-input", value: draft, onChange: (e) => setDraft(e.target.value), onKeyDown: onKeyDown, placeholder: status === 'idle' || status === 'loading'
570
+ ? 'Preparing chat…'
571
+ : inputPlaceholder ?? 'Type a message…', rows: 1,
550
572
  // The textarea stays editable while the agent is
551
573
  // streaming so the visitor can compose their next
552
574
  // message without waiting. Only block when the
@@ -563,7 +585,12 @@ function ChatWidget(props) {
563
585
  // jumps to <body>, and the on-screen keyboard collapses.
564
586
  onPointerDown: (e) => e.preventDefault(), onClick: handleSend, disabled: sendDisabled, "aria-label": status === 'sending' || status === 'streaming'
565
587
  ? 'Sending message'
566
- : 'Send message', children: status === 'sending' || status === 'streaming' ? ((0, jsx_runtime_1.jsx)(SpinnerIcon, {})) : ((0, jsx_runtime_1.jsx)(SendIcon, {})) })] }), !bare && (0, jsx_runtime_1.jsx)("div", { className: "af-footer", children: "Powered by AgentForge" })] })] }));
588
+ : status === 'idle' || status === 'loading'
589
+ ? 'Preparing chat'
590
+ : 'Send message', children: status === 'sending' ||
591
+ status === 'streaming' ||
592
+ status === 'idle' ||
593
+ status === 'loading' ? ((0, jsx_runtime_1.jsx)(SpinnerIcon, {})) : ((0, jsx_runtime_1.jsx)(SendIcon, {})) })] }), !bare && (0, jsx_runtime_1.jsx)("div", { className: "af-footer", children: "Powered by AgentForge" })] })] }));
567
594
  }
568
595
  function MessageBubble({ message, session, readOnly, onDecision, onContinue, bare = false, showAvatar = false, avatarTheme, avatarName, avatarAgentId, speakerLabel, }) {
569
596
  const kind = message.metadata?.kind;
@@ -825,6 +852,8 @@ const WIDGET_CSS = `
825
852
  to { opacity: 1; transform: translateY(0); }
826
853
  }
827
854
  .af-input-row { padding: 12px; border-top: 1px solid var(--af-border); background: var(--af-bg); display: flex; gap: 8px; align-items: flex-end; }
855
+ .af-input-row[data-loading] .af-input { cursor: progress; opacity: 0.7; }
856
+ .af-input-row[data-loading] .af-input::placeholder { font-style: italic; }
828
857
  /* Composer left slot — hosts use this for affordance buttons that
829
858
  scope the next turn (member picker, tools menu, attachments).
830
859
  align-items: center keeps a single-line chip vertically centered
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/chat-sdk",
3
- "version": "2.4.0-dev.11",
3
+ "version": "2.4.0-dev.13",
4
4
  "description": "Framework-free chat session SDK for AgentForge public chat tokens. Headless — no DOM. Drop into any frontend (React, Vue, Svelte, vanilla) and listen for events.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",