@agentforge-io/chat-sdk 2.4.1 → 2.5.1

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 +243 -52
  2. package/package.json +1 -1
package/dist/react.js CHANGED
@@ -517,51 +517,153 @@ function ChatWidget(props) {
517
517
  };
518
518
  return ((0, jsx_runtime_1.jsxs)("div", { className: rootClass, style: rootStyle, "data-style-id": styleId, children: [!inline && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-toggle", "aria-label": open ? 'Close chat' : 'Open chat', "aria-expanded": open, onClick: () => setOpen((v) => !v), children: open ? (0, jsx_runtime_1.jsx)(CloseIcon, {}) : (0, jsx_runtime_1.jsx)(ChatIcon, {}) })), (0, jsx_runtime_1.jsxs)("div", { className: `af-panel ${open ? 'af-open' : ''}`, children: [!bare && ((0, jsx_runtime_1.jsxs)("div", { className: "af-header", children: [theme?.avatarUrl ? (
519
519
  // eslint-disable-next-line @next/next/no-img-element
520
- (0, jsx_runtime_1.jsx)("img", { className: "af-header-avatar", src: theme.avatarUrl, alt: "" })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "af-header-info", children: [(0, jsx_runtime_1.jsx)("div", { className: "af-header-title", children: personaName ?? theme?.title ?? agent?.name ?? 'Chat' }), (0, jsx_runtime_1.jsx)("div", { className: "af-header-subtitle", children: status === 'loading' ? 'Loading…' : agent?.description ?? '' })] }), !inline && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-close", onClick: () => setOpen(false), "aria-label": "Close chat", children: (0, jsx_runtime_1.jsx)(CloseIcon, {}) }))] })), (0, jsx_runtime_1.jsx)("div", { className: "af-messages", ref: messagesRef, children: messages.map((m, i) => {
521
- // Show the avatar only on the FIRST bubble in a run of
522
- // assistant messages BY THE SAME speaker. A delegation
523
- // boundary (orchestrator member, or member → next
524
- // member) counts as a new run so each speaker gets their
525
- // avatar surfaced. User/system bubbles never get an avatar.
526
- const prev = messages[i - 1];
527
- const isAssistant = m.role === 'assistant';
528
- const sameAssistantRun = prev?.role === 'assistant' &&
529
- (prev?.actingAgentId ?? null) === (m.actingAgentId ?? null);
530
- const showAvatar = bare && isAssistant && !sameAssistantRun;
531
- // Per-bubble identity resolution. When the chunk carried
532
- // an `actingAgentId` (orchestrator delegated to a member),
533
- // we render the member's avatar/name instead of the
534
- // primary agent's. Falling back to the primary identity
535
- // keeps non-team sessions and orchestrator-self turns
536
- // looking the same as before.
537
- const member = m.actingAgentId
538
- ? membersById.get(m.actingAgentId)
539
- : undefined;
540
- const bubbleAvatarTheme = member
541
- ? { ...theme, avatarUrl: member.avatarUrl }
542
- : theme;
543
- const bubbleAvatarName = member?.name ?? personaName ?? agent?.name;
544
- // Seed for the deterministic-hue fallback avatar. Prefer
545
- // the acting agent id (so each member in a team gets its
546
- // own stable color) and fall back to the primary agent's
547
- // slug for solo chats / orchestrator-self turns. The
548
- // `agent` summary doesn't carry an id — slug is stable
549
- // and unique, which is all hueFromSeed needs.
550
- const bubbleAgentId = m.actingAgentId ?? agent?.slug;
551
- return ((0, jsx_runtime_1.jsx)(MessageBubble, { message: m, session: session, readOnly: readOnlyApprovals, onDecision: onApprovalDecision, bare: bare, showAvatar: showAvatar, avatarTheme: bubbleAvatarTheme, avatarName: bubbleAvatarName, avatarAgentId: bubbleAgentId, speakerLabel: member?.name, onContinue: () => {
552
- // After a successful Approve, kick the next turn so the
553
- // gate's fast-path consumes the approval and the tool
554
- // actually runs. `silent: true` keeps the literal
555
- // "continue" prompt out of the visible transcript —
556
- // the visitor just sees the assistant's next reply
557
- // appearing under the "Approved" pill.
558
- if (!session)
559
- return;
560
- setTimeout(() => {
561
- void session.send('continue', { silent: true });
562
- }, 250);
563
- } }, m.id));
564
- }) }), lastError && status === 'error' && ((0, jsx_runtime_1.jsx)("div", { className: "af-error", children: lastError })), bare && greeting && messages.length === 0 && status !== 'loading' && ((0, jsx_runtime_1.jsx)("div", { className: "af-greeting-slot", children: (0, jsx_runtime_1.jsxs)("div", { className: "af-msg-row af-msg-row-assistant af-msg-row-greeting", children: [(0, jsx_runtime_1.jsx)(AssistantAvatar, { theme: theme, name: personaName ?? agent?.name, agentId: agent?.slug, show: true }), (0, jsx_runtime_1.jsx)("div", { className: "af-msg af-msg-assistant af-msg-greeting", dangerouslySetInnerHTML: { __html: renderMarkdown(greeting) } })] }) })), shortcuts && shortcuts.length > 0 && messages.length === 0 && ((0, jsx_runtime_1.jsx)("div", { className: "af-shortcut-row", children: shortcuts.map((text, i) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-shortcut", onClick: () => {
520
+ (0, jsx_runtime_1.jsx)("img", { className: "af-header-avatar", src: theme.avatarUrl, alt: "" })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "af-header-info", children: [(0, jsx_runtime_1.jsx)("div", { className: "af-header-title", children: personaName ?? theme?.title ?? agent?.name ?? 'Chat' }), (0, jsx_runtime_1.jsx)("div", { className: "af-header-subtitle", children: status === 'loading' ? 'Loading…' : agent?.description ?? '' })] }), !inline && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-close", onClick: () => setOpen(false), "aria-label": "Close chat", children: (0, jsx_runtime_1.jsx)(CloseIcon, {}) }))] })), (0, jsx_runtime_1.jsx)("div", { className: "af-messages", ref: messagesRef, children: (() => {
521
+ // Pre-walk the transcript to identify "delegation chains":
522
+ // a sequence of consecutive assistant bubbles where each has
523
+ // a different `actingAgentId` AND every intermediate bubble
524
+ // is empty (the orchestrator/member delegated immediately
525
+ // and never produced text of its own). The visible bubble
526
+ // is the LAST one — the speaker that actually answered.
527
+ //
528
+ // For each chain we render a single MessageBubble for the
529
+ // last entry, with the avatar slot replaced by a HORIZONTAL
530
+ // STACK of every participant in delegation order: the
531
+ // active speaker first (largest), the upstream delegators
532
+ // smaller and overlapped to the right.
533
+ //
534
+ // chainHeadFor[i] holds the head index of the chain that
535
+ // contains message i. visibleIdxs is the list of indices
536
+ // we actually render.
537
+ const chainHeadFor = new Array(messages.length);
538
+ const visibleIdxs = [];
539
+ for (let i = 0; i < messages.length; i++) {
540
+ const m = messages[i];
541
+ if (m.role !== 'assistant') {
542
+ chainHeadFor[i] = i;
543
+ visibleIdxs.push(i);
544
+ continue;
545
+ }
546
+ const prev = messages[i - 1];
547
+ const prevSameSpeaker = prev?.role === 'assistant' &&
548
+ (prev?.actingAgentId ?? null) === (m.actingAgentId ?? null);
549
+ // Walk back through the chain: pick the head of the
550
+ // previous bubble's chain if that bubble was an empty
551
+ // assistant with a DIFFERENT actingAgentId (delegation
552
+ // boundary). Same speaker -> head stays the same.
553
+ if (prev?.role === 'assistant' &&
554
+ (prev.actingAgentId ?? null) !== (m.actingAgentId ?? null) &&
555
+ !prev.content) {
556
+ chainHeadFor[i] = chainHeadFor[i - 1];
557
+ }
558
+ else if (prevSameSpeaker) {
559
+ chainHeadFor[i] = chainHeadFor[i - 1];
560
+ }
561
+ else {
562
+ chainHeadFor[i] = i;
563
+ }
564
+ visibleIdxs.push(i);
565
+ }
566
+ // Find the index of the next assistant message after each
567
+ // position. Used to decide whether an empty bubble has been
568
+ // superseded by a later speaker (in which case the visitor
569
+ // should not still see it typing).
570
+ const nextAssistantIdx = new Array(messages.length).fill(-1);
571
+ for (let k = messages.length - 2; k >= 0; k--) {
572
+ const nextI = k + 1;
573
+ nextAssistantIdx[k] =
574
+ messages[nextI].role === 'assistant'
575
+ ? nextI
576
+ : nextAssistantIdx[nextI];
577
+ }
578
+ const finalIdxs = visibleIdxs.filter((i) => {
579
+ const m = messages[i];
580
+ if (m.role !== 'assistant')
581
+ return true;
582
+ // Any empty assistant bubble that has been overtaken by
583
+ // a LATER assistant bubble is an intermediate — hide it.
584
+ // Covers two cases at once:
585
+ // 1. The classic "chain head/intermediate" path: bubble
586
+ // i is empty and there's a final bubble after it.
587
+ // 2. The "live delegation" path: while a chain streams,
588
+ // the previous member sits at content='' isStreaming=
589
+ // true (the SDK only flips isStreaming when the
590
+ // bubble produced text). A naive filter would keep
591
+ // showing its typing dots; we drop it as soon as
592
+ // a fresher speaker exists.
593
+ if (!m.content && nextAssistantIdx[i] !== -1)
594
+ return false;
595
+ return true;
596
+ });
597
+ return finalIdxs.map((i) => {
598
+ const m = messages[i];
599
+ const prev = messages[i - 1];
600
+ const isAssistant = m.role === 'assistant';
601
+ const sameAssistantRun = prev?.role === 'assistant' &&
602
+ (prev?.actingAgentId ?? null) === (m.actingAgentId ?? null);
603
+ const showAvatar = bare && isAssistant && !sameAssistantRun;
604
+ // Per-bubble identity resolution.
605
+ const member = m.actingAgentId
606
+ ? membersById.get(m.actingAgentId)
607
+ : undefined;
608
+ const bubbleAvatarTheme = member
609
+ ? { ...theme, avatarUrl: member.avatarUrl }
610
+ : theme;
611
+ const bubbleAvatarName = member?.name ?? personaName ?? agent?.name;
612
+ const bubbleAgentId = m.actingAgentId ?? agent?.slug;
613
+ // Build the delegation-chain participants list when this
614
+ // bubble is the LAST in a chain (= the speaker that
615
+ // answered). Order: active speaker first, then the prior
616
+ // delegators in REVERSE chronological order so the most
617
+ // recent delegator sits closest to the active avatar.
618
+ let chainParticipants;
619
+ if (isAssistant && showAvatar) {
620
+ const head = chainHeadFor[i];
621
+ // Collect all distinct speakers in [head…i] in
622
+ // chronological order. The bubble that's rendering now
623
+ // (i) is the most recent speaker so it becomes the
624
+ // "active" one at the front of the stack; everything
625
+ // earlier in the chain trails behind in reverse-
626
+ // chronological order. Works the same whether the
627
+ // chain has finished or is still streaming — visitor
628
+ // sees one bubble with a live-updating header.
629
+ const seen = new Set();
630
+ const ordered = [];
631
+ for (let j = head; j <= i; j++) {
632
+ const mj = messages[j];
633
+ if (mj.role !== 'assistant')
634
+ continue;
635
+ const key = mj.actingAgentId ?? null;
636
+ if (seen.has(key))
637
+ continue;
638
+ seen.add(key);
639
+ const mem = mj.actingAgentId
640
+ ? membersById.get(mj.actingAgentId)
641
+ : undefined;
642
+ ordered.push({
643
+ agentId: mj.actingAgentId ?? agent?.slug,
644
+ name: mem?.name ?? personaName ?? agent?.name,
645
+ theme: mem
646
+ ? { ...theme, avatarUrl: mem.avatarUrl }
647
+ : theme,
648
+ });
649
+ }
650
+ if (ordered.length > 1) {
651
+ // The visible bubble's speaker (i) sits at the END of
652
+ // `ordered`. Move them to the front so the active
653
+ // avatar leads the stack.
654
+ const active = ordered.pop();
655
+ chainParticipants = [active, ...ordered.reverse()];
656
+ }
657
+ }
658
+ return ((0, jsx_runtime_1.jsx)(MessageBubble, { message: m, session: session, readOnly: readOnlyApprovals, onDecision: onApprovalDecision, bare: bare, showAvatar: showAvatar, avatarTheme: bubbleAvatarTheme, avatarName: bubbleAvatarName, avatarAgentId: bubbleAgentId, speakerLabel: member?.name, chainParticipants: chainParticipants, onContinue: () => {
659
+ if (!session)
660
+ return;
661
+ setTimeout(() => {
662
+ void session.send('continue', { silent: true });
663
+ }, 250);
664
+ } }, m.id));
665
+ });
666
+ })() }), lastError && status === 'error' && ((0, jsx_runtime_1.jsx)("div", { className: "af-error", children: lastError })), bare && greeting && messages.length === 0 && status !== 'loading' && ((0, jsx_runtime_1.jsx)("div", { className: "af-greeting-slot", children: (0, jsx_runtime_1.jsxs)("div", { className: "af-msg-row af-msg-row-assistant af-msg-row-greeting", children: [(0, jsx_runtime_1.jsx)(AssistantAvatar, { theme: theme, name: personaName ?? agent?.name, agentId: agent?.slug, show: true }), (0, jsx_runtime_1.jsx)("div", { className: "af-msg af-msg-assistant af-msg-greeting", dangerouslySetInnerHTML: { __html: renderMarkdown(greeting) } })] }) })), shortcuts && shortcuts.length > 0 && messages.length === 0 && ((0, jsx_runtime_1.jsx)("div", { className: "af-shortcut-row", children: shortcuts.map((text, i) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-shortcut", onClick: () => {
565
667
  if (onShortcutClick)
566
668
  onShortcutClick(text, i);
567
669
  else
@@ -592,16 +694,16 @@ function ChatWidget(props) {
592
694
  status === 'idle' ||
593
695
  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" })] })] }));
594
696
  }
595
- function MessageBubble({ message, session, readOnly, onDecision, onContinue, bare = false, showAvatar = false, avatarTheme, avatarName, avatarAgentId, speakerLabel, }) {
697
+ function MessageBubble({ message, session, readOnly, onDecision, onContinue, bare = false, showAvatar = false, avatarTheme, avatarName, avatarAgentId, speakerLabel, chainParticipants, }) {
596
698
  const kind = message.metadata?.kind;
597
699
  if (kind === 'awaiting_approval') {
598
700
  // Approval / blocked bubbles also count as "assistant-side" so we
599
701
  // wrap them in the same row geometry — keeps the conversation
600
702
  // aligned even when a tool dispatch interrupts the regular flow.
601
- return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, avatarAgentId, (0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, session: session, readOnly: readOnly, onDecision: onDecision, onContinue: onContinue }), speakerLabel);
703
+ return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, avatarAgentId, (0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, session: session, readOnly: readOnly, onDecision: onDecision, onContinue: onContinue }), speakerLabel, chainParticipants);
602
704
  }
603
705
  if (kind === 'tool_blocked') {
604
- return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, avatarAgentId, (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message }), speakerLabel);
706
+ return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, avatarAgentId, (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message }), speakerLabel, chainParticipants);
605
707
  }
606
708
  const cls = `af-msg af-msg-${message.role}${message.role === 'assistant' && message.isStreaming
607
709
  ? message.content
@@ -610,13 +712,13 @@ function MessageBubble({ message, session, readOnly, onDecision, onContinue, bar
610
712
  : ''}`;
611
713
  // Typing state (no content yet): render the three-dot indicator, no markdown.
612
714
  if (message.role === 'assistant' && message.isStreaming && !message.content) {
613
- return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, avatarAgentId, (0, jsx_runtime_1.jsx)("div", { className: cls, children: (0, jsx_runtime_1.jsxs)("span", { className: "af-typing-dots", "aria-label": "Assistant is typing", children: [(0, jsx_runtime_1.jsx)("span", {}), " ", (0, jsx_runtime_1.jsx)("span", {}), " ", (0, jsx_runtime_1.jsx)("span", {})] }) }), speakerLabel);
715
+ return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, avatarAgentId, (0, jsx_runtime_1.jsx)("div", { className: cls, children: (0, jsx_runtime_1.jsxs)("span", { className: "af-typing-dots", "aria-label": "Assistant is typing", children: [(0, jsx_runtime_1.jsx)("span", {}), " ", (0, jsx_runtime_1.jsx)("span", {}), " ", (0, jsx_runtime_1.jsx)("span", {})] }) }), speakerLabel, chainParticipants);
614
716
  }
615
717
  if (message.role === 'assistant') {
616
718
  return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, avatarAgentId, (0, jsx_runtime_1.jsx)("div", { className: cls,
617
719
  // Output is sanitized by escapeHtml + a fixed tag whitelist in
618
720
  // renderMarkdown — safe to inject as HTML.
619
- dangerouslySetInnerHTML: { __html: renderMarkdown(message.content) } }), speakerLabel);
721
+ dangerouslySetInnerHTML: { __html: renderMarkdown(message.content) } }), speakerLabel, chainParticipants);
620
722
  }
621
723
  // User & system messages stay as plain text — they're typed verbatim.
622
724
  // No avatar column on the user side; they align right.
@@ -631,10 +733,15 @@ function wrapAssistantRow(bare, showAvatar, theme, name, agentId, child,
631
733
  /** When set + this is the first bubble of a speaker run (showAvatar
632
734
  * is true), render the speaker's display name just above the
633
735
  * bubble. Used by Team chats so members are visually attributed. */
634
- speakerLabel) {
736
+ speakerLabel,
737
+ /** When set, replace the single avatar with a horizontal stack of
738
+ * every speaker in the delegation chain that produced this bubble.
739
+ * Index 0 is the active speaker (largest), the rest are upstream
740
+ * delegators in reverse-chronological order. */
741
+ chainParticipants) {
635
742
  if (!bare)
636
743
  return child;
637
- return ((0, jsx_runtime_1.jsxs)("div", { className: "af-msg-row af-msg-row-assistant", children: [(0, jsx_runtime_1.jsx)(AssistantAvatar, { theme: theme, name: name, agentId: agentId, show: showAvatar }), (0, jsx_runtime_1.jsxs)("div", { className: "af-msg-col", children: [showAvatar && speakerLabel ? ((0, jsx_runtime_1.jsx)("div", { className: "af-msg-speaker", children: speakerLabel })) : null, child] })] }));
744
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "af-msg-row af-msg-row-assistant", children: [chainParticipants && chainParticipants.length > 1 ? ((0, jsx_runtime_1.jsx)(DelegationAvatarStack, { participants: chainParticipants })) : ((0, jsx_runtime_1.jsx)(AssistantAvatar, { theme: theme, name: name, agentId: agentId, show: showAvatar })), (0, jsx_runtime_1.jsxs)("div", { className: "af-msg-col", children: [showAvatar && speakerLabel ? ((0, jsx_runtime_1.jsx)("div", { className: "af-msg-speaker", children: speakerLabel })) : null, child] })] }));
638
745
  }
639
746
  /**
640
747
  * Small circular avatar for the assistant-side column. Prefers the
@@ -660,6 +767,36 @@ function AssistantAvatar({ theme, name, agentId, show, }) {
660
767
  const seed = agentId || name || 'agent';
661
768
  return ((0, jsx_runtime_1.jsx)("div", { className: "af-msg-avatar af-msg-avatar-fallback", "aria-hidden": true, style: { backgroundColor: hueFromSeed(seed) }, children: (0, jsx_runtime_1.jsx)("span", { children: initial }) }));
662
769
  }
770
+ /**
771
+ * Horizontal stack of avatars representing the delegation chain that
772
+ * produced the current bubble. Index 0 is the active speaker — drawn
773
+ * at the regular avatar size on the LEFT (the speaker that actually
774
+ * answered, attention sits on them). The remaining participants are
775
+ * upstream delegators in reverse-chronological order: each one
776
+ * slightly smaller and overlapped to the right of the active avatar.
777
+ *
778
+ * Visually: [active] [delegator-1] [delegator-2] [...]
779
+ * Mental model: "this answer came from <active>, delegated by <…>".
780
+ */
781
+ function DelegationAvatarStack({ participants, }) {
782
+ return ((0, jsx_runtime_1.jsx)("div", { className: "af-msg-avatar af-msg-avatar-stack", role: "img", "aria-label": `Delegation chain: ${participants.map((p) => p.name).filter(Boolean).join(' → ')}`, children: participants.map((p, idx) => {
783
+ const isActive = idx === 0;
784
+ const initial = (p.name ?? 'A').trim().charAt(0).toUpperCase() || 'A';
785
+ const seed = p.agentId || p.name || `agent-${idx}`;
786
+ const url = p.theme?.avatarUrl;
787
+ const className = 'af-msg-avatar-stack-item' +
788
+ (isActive ? ' af-msg-avatar-stack-active' : '');
789
+ // Inline z-index so the leftmost (active) lands on top of the
790
+ // overlapping siblings to its right.
791
+ const z = participants.length - idx;
792
+ return url ? (
793
+ // eslint-disable-next-line @next/next/no-img-element
794
+ (0, jsx_runtime_1.jsx)("img", { className: className + ' af-msg-avatar-stack-img', src: url, alt: "", "aria-hidden": true, style: { zIndex: z } }, `${idx}-${seed}`)) : ((0, jsx_runtime_1.jsx)("div", { className: className + ' af-msg-avatar-stack-fallback', "aria-hidden": true, style: {
795
+ backgroundColor: hueFromSeed(seed),
796
+ zIndex: z,
797
+ }, children: (0, jsx_runtime_1.jsx)("span", { children: initial }) }, `${idx}-${seed}`));
798
+ }) }));
799
+ }
663
800
  /**
664
801
  * Deterministic per-agent hue. Hash the seed into the HSL hue space
665
802
  * so an agent's avatar color stays stable across renders and looks
@@ -1229,6 +1366,60 @@ const WIDGET_CSS = `
1229
1366
  * inline color and made every agent avatar look the same. */
1230
1367
  background-color: var(--af-primary, #8b5cf6);
1231
1368
  }
1369
+ /* Delegation-chain avatar stack. Override the default 26x26 size so
1370
+ * the row can host multiple overlapping items without clipping. */
1371
+ .af-widget-root.af-variant-bare .af-msg-avatar-stack {
1372
+ width: auto;
1373
+ height: 26px;
1374
+ border-radius: 0;
1375
+ overflow: visible;
1376
+ display: flex;
1377
+ flex-direction: row;
1378
+ align-items: flex-end;
1379
+ /* Slight padding-right so the rightmost stacked item doesn't sit
1380
+ * flush against the bubble bevel. */
1381
+ padding-right: 4px;
1382
+ }
1383
+ .af-widget-root.af-variant-bare .af-msg-avatar-stack-item {
1384
+ width: 18px;
1385
+ height: 18px;
1386
+ border-radius: 50%;
1387
+ overflow: hidden;
1388
+ display: flex;
1389
+ align-items: center;
1390
+ justify-content: center;
1391
+ flex-shrink: 0;
1392
+ /* 2px ring matching the bubble background so overlapping reads
1393
+ * as distinct circles, not a continuous blob. */
1394
+ box-shadow: 0 0 0 2px var(--af-bg, #ffffff);
1395
+ /* Overlap with the prior sibling. */
1396
+ margin-left: -6px;
1397
+ }
1398
+ .af-widget-root.af-variant-bare .af-msg-avatar-stack-item:first-child {
1399
+ margin-left: 0;
1400
+ }
1401
+ .af-widget-root.af-variant-bare .af-msg-avatar-stack-active {
1402
+ /* The active speaker is rendered full-size so it visually anchors
1403
+ * the chain. Slightly larger ring keeps it clearly separated from
1404
+ * the smaller upstream delegators. */
1405
+ width: 26px;
1406
+ height: 26px;
1407
+ }
1408
+ .af-widget-root.af-variant-bare .af-msg-avatar-stack-img {
1409
+ object-fit: cover;
1410
+ display: block;
1411
+ width: 100%;
1412
+ height: 100%;
1413
+ }
1414
+ .af-widget-root.af-variant-bare .af-msg-avatar-stack-fallback {
1415
+ color: #fff;
1416
+ font-size: 9.5px;
1417
+ font-weight: 600;
1418
+ background-color: var(--af-primary, #8b5cf6);
1419
+ }
1420
+ .af-widget-root.af-variant-bare .af-msg-avatar-stack-active.af-msg-avatar-stack-fallback {
1421
+ font-size: 11.5px;
1422
+ }
1232
1423
  @media (min-width: 768px) {
1233
1424
  .af-widget-root.af-variant-bare .af-msg-avatar { width: 30px; height: 30px; }
1234
1425
  .af-widget-root.af-variant-bare .af-msg-avatar-fallback { font-size: 13px; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/chat-sdk",
3
- "version": "2.4.1",
3
+ "version": "2.5.1",
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",