@skippr/live-agent-sdk 0.102.0 → 0.104.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/README.md CHANGED
@@ -56,7 +56,6 @@ The widget opens with a picker of your active modules — the user chooses which
56
56
 
57
57
  ```tsx
58
58
  import { LiveAgent } from '@skippr/live-agent-sdk';
59
- import '@skippr/live-agent-sdk/styles';
60
59
 
61
60
  function App() {
62
61
  return <LiveAgent appKey="pk_live_your_key" />;
@@ -3745,6 +3745,9 @@ function mapApiMessages(messages) {
3745
3745
  timestamp: new Date(message.createdAt).getTime()
3746
3746
  }));
3747
3747
  }
3748
+ function hasLiveSession(flags) {
3749
+ return flags.isConnected || flags.isStarting || flags.isPausing || flags.isDisconnecting || flags.isExchanging;
3750
+ }
3748
3751
  function getResumableSessionId(currentSession, mode) {
3749
3752
  if (!currentSession)
3750
3753
  return;
@@ -9640,17 +9643,8 @@ function useAdoptionTips() {
9640
9643
  if (!ctx) {
9641
9644
  throw new Error("useAdoptionTips must be used within a <LiveAgent> provider");
9642
9645
  }
9643
- const {
9644
- adoptionGoals,
9645
- agentMode,
9646
- hasResolvedModules,
9647
- isConnected,
9648
- isStarting,
9649
- isPausing,
9650
- isDisconnecting
9651
- } = ctx;
9652
- const sessionLive = isConnected || isStarting || isPausing || isDisconnecting;
9653
- const inStandardSession = sessionLive && agentMode !== AGENT_MODE.AlwaysOn;
9646
+ const { adoptionGoals, agentMode, hasResolvedModules } = ctx;
9647
+ const inStandardSession = hasLiveSession(ctx) && agentMode !== AGENT_MODE.AlwaysOn;
9654
9648
  if (inStandardSession || !hasResolvedModules || adoptionGoals.length === 0)
9655
9649
  return [];
9656
9650
  return adoptionGoals.slice(0, MAX_ADOPTION_TIPS);
@@ -9670,11 +9664,13 @@ function useBarNotifications() {
9670
9664
  hasResolvedModules,
9671
9665
  isConnected,
9672
9666
  previewStart,
9667
+ selectModule,
9673
9668
  attendGoal
9674
9669
  } = ctx;
9675
9670
  const tips = useAdoptionTips();
9676
9671
  const { sendChatMessage } = useChatMessages();
9677
9672
  const inAlwaysOnSession = isConnected && agentMode === AGENT_MODE.AlwaysOn;
9673
+ const inSession = hasLiveSession(ctx);
9678
9674
  const tipNotifications = tips.map((goal) => ({
9679
9675
  id: goal.goalId,
9680
9676
  kind: NOTIFICATION_KIND.Tip,
@@ -9693,7 +9689,7 @@ function useBarNotifications() {
9693
9689
  kind: NOTIFICATION_KIND.Module,
9694
9690
  message: module.name,
9695
9691
  accentColor: MODULE_ACCENT_COLOR,
9696
- activate: () => previewStart({ kind: PENDING_START_KIND.Module, moduleId: module.id })
9692
+ activate: inSession ? () => void selectModule(module.id) : () => previewStart({ kind: PENDING_START_KIND.Module, moduleId: module.id })
9697
9693
  }));
9698
9694
  return [...tipNotifications, ...moduleNotifications];
9699
9695
  }
@@ -10613,8 +10609,8 @@ function VoiceBarFrame({
10613
10609
  const {
10614
10610
  isConnected,
10615
10611
  isStarting,
10612
+ isExchanging,
10616
10613
  isPausing,
10617
- isDisconnecting,
10618
10614
  isAuthenticated,
10619
10615
  usesHostAuth,
10620
10616
  defaultAgentId,
@@ -10709,7 +10705,8 @@ function VoiceBarFrame({
10709
10705
  code: "Space",
10710
10706
  requireCtrl: true
10711
10707
  });
10712
- const sessionLive = isConnected || isStarting || isPausing || isDisconnecting;
10708
+ const sessionLive = hasLiveSession(ctx);
10709
+ const isLoading = isStarting || isExchanging;
10713
10710
  const canStart = isAuthenticated && (!!defaultAgentId || !!pendingStart);
10714
10711
  const showClose = canFold || sessionLive;
10715
10712
  const logoUrl = appearance?.logoUrl ?? null;
@@ -10725,7 +10722,7 @@ function VoiceBarFrame({
10725
10722
  minimizePanel();
10726
10723
  }
10727
10724
  };
10728
- useKeyboardStop({ enabled: sessionLive, onStop: closeBar });
10725
+ useKeyboardStop({ enabled: sessionLive && !isPausing && !isLoading, onStop: closeBar });
10729
10726
  const hintSeenRef = useRef39(false);
10730
10727
  const requestPillBg = isCapturingSpeech ? "var(--skippr-brand-active)" : "var(--skippr-brand, #2bc0ae)";
10731
10728
  const agentStateLabel = state === "thinking" ? "thinking…" : state === "speaking" ? "talking…" : null;
@@ -10777,7 +10774,7 @@ function VoiceBarFrame({
10777
10774
  const linkCardActive = !!visibleLinkCard && isConnected && !planCardActive && !(isRoomChat && chatOpen) && !isCapturingSpeech && !isHeld && !autoOffClosing;
10778
10775
  const bannerActive = hasNotifications && bannerOpen;
10779
10776
  const slotClaimed = chatOpen || bannerActive || lineBlockOpen;
10780
- const autoBase = !isStarting && isConnected && !textMode && !pushOrTap && !autoOffClosing;
10777
+ const autoBase = !isLoading && isConnected && !textMode && !pushOrTap && !autoOffClosing;
10781
10778
  const showHeldStrip = !slotClaimed && autoBase && isHeld;
10782
10779
  const showLiveStrip = !slotClaimed && autoBase && !isHeld && !planCardActive && (state === "thinking" || showActionControl);
10783
10780
  const showTimerStrip = !slotClaimed && !autoOffClosing && !isCapturingSpeech && !planCardActive && !showLiveStrip && !showHeldStrip && !!sessionTimer;
@@ -10980,9 +10977,9 @@ function VoiceBarFrame({
10980
10977
  showClose && /* @__PURE__ */ jsxs37("button", {
10981
10978
  type: "button",
10982
10979
  onClick: ifNotDragged(closeBar),
10983
- disabled: isPausing || isStarting,
10980
+ disabled: isPausing || isLoading,
10984
10981
  "aria-label": sessionLive ? "End session" : "Close",
10985
- className: cn(EDGE_BUTTON, "skippr:relative skippr:size-[26px]", isStarting && "skippr:cursor-not-allowed skippr:opacity-40 skippr:hover:bg-transparent skippr:hover:text-[#b8b4ac]"),
10982
+ className: cn(EDGE_BUTTON, "skippr:relative skippr:size-[26px]", isLoading && "skippr:cursor-not-allowed skippr:opacity-40 skippr:hover:bg-transparent skippr:hover:text-[#b8b4ac]"),
10986
10983
  children: [
10987
10984
  isPausing && /* @__PURE__ */ jsx45(BusyRing, {
10988
10985
  tone: "brand"
@@ -10993,8 +10990,8 @@ function VoiceBarFrame({
10993
10990
  })
10994
10991
  ]
10995
10992
  }),
10996
- isStarting && /* @__PURE__ */ jsxs37("div", {
10997
- className: cn(PILL, "skippr:bg-[#F1EFEA] skippr:overflow-hidden skippr:[animation:skippr-fade-in_0.25s_ease]"),
10993
+ isLoading && /* @__PURE__ */ jsxs37("div", {
10994
+ className: cn(PILL, "skippr:bg-[#F1EFEA] skippr:overflow-hidden", !isExchanging && "skippr:[animation:skippr-fade-in_0.25s_ease]"),
10998
10995
  children: [
10999
10996
  /* @__PURE__ */ jsx45(LoadingAvatar, {
11000
10997
  logoUrl
@@ -11019,7 +11016,7 @@ function VoiceBarFrame({
11019
11016
  })
11020
11017
  ]
11021
11018
  }),
11022
- !isStarting && isConnected && !textMode && pushOrTap && /* @__PURE__ */ jsx45(ShortcutHint, {
11019
+ !isLoading && isConnected && !textMode && pushOrTap && /* @__PURE__ */ jsx45(ShortcutHint, {
11023
11020
  enabled: isPushToTalk,
11024
11021
  seenRef: hintSeenRef,
11025
11022
  suppressed: hintSuppressed,
@@ -11050,7 +11047,7 @@ function VoiceBarFrame({
11050
11047
  ]
11051
11048
  })
11052
11049
  }),
11053
- !isStarting && isConnected && !textMode && !pushOrTap && /* @__PURE__ */ jsxs37("div", {
11050
+ !isLoading && isConnected && !textMode && !pushOrTap && /* @__PURE__ */ jsxs37("div", {
11054
11051
  style: { background: livePillBg, color: "var(--skippr-brand-foreground, #2d2b3d)" },
11055
11052
  className: cn(PILL, "skippr:gap-[9px] skippr:px-[8px]"),
11056
11053
  children: [
@@ -11123,7 +11120,7 @@ function VoiceBarFrame({
11123
11120
  })
11124
11121
  ]
11125
11122
  }),
11126
- !isStarting && isConnected && textMode && /* @__PURE__ */ jsxs37("div", {
11123
+ !isLoading && isConnected && textMode && /* @__PURE__ */ jsxs37("div", {
11127
11124
  className: cn(PILL, "skippr:gap-[7px] skippr:bg-[#F1EFEA] skippr:text-[#5a5650] skippr:[animation:skippr-fade-in_0.25s_ease]"),
11128
11125
  children: [
11129
11126
  /* @__PURE__ */ jsx45(MessageSquare, {
@@ -11164,9 +11161,9 @@ function VoiceBarFrame({
11164
11161
  (sessionLive || isRoomChat) && /* @__PURE__ */ jsx45("button", {
11165
11162
  type: "button",
11166
11163
  onClick: ifNotDragged(toggleChat),
11167
- disabled: isStarting,
11164
+ disabled: isLoading,
11168
11165
  "aria-label": "Open chat",
11169
- className: cn(EDGE_BUTTON, "skippr:size-[30px] skippr:text-[#8b877f]", isStarting && "skippr:cursor-not-allowed skippr:opacity-40 skippr:hover:bg-transparent skippr:hover:text-[#8b877f]"),
11166
+ className: cn(EDGE_BUTTON, "skippr:size-[30px] skippr:text-[#8b877f]", isLoading && "skippr:cursor-not-allowed skippr:opacity-40 skippr:hover:bg-transparent skippr:hover:text-[#8b877f]"),
11170
11167
  children: /* @__PURE__ */ jsx45(MessageSquare, {
11171
11168
  className: "skippr:size-[15px]",
11172
11169
  strokeWidth: 2
@@ -11662,10 +11659,6 @@ function VoiceBar({ revealed }) {
11662
11659
  throw new Error("VoiceBar must be used within a <LiveAgent> provider");
11663
11660
  }
11664
11661
  const {
11665
- isConnected,
11666
- isStarting,
11667
- isPausing,
11668
- isDisconnecting,
11669
11662
  isMinimized,
11670
11663
  isAuthenticated,
11671
11664
  usesHostAuth,
@@ -11687,7 +11680,7 @@ function VoiceBar({ revealed }) {
11687
11680
  if (!revealed)
11688
11681
  return null;
11689
11682
  const launcherMode = appearance?.launcherMode ?? DEFAULT_LAUNCHER_MODE;
11690
- const sessionLive = isConnected || isStarting || isPausing || isDisconnecting;
11683
+ const sessionLive = hasLiveSession(ctx);
11691
11684
  const canFold = launcherMode !== LAUNCHER_MODE.None;
11692
11685
  const needsLogin = !usesHostAuth && !isAuthenticated;
11693
11686
  const launcherEdge = appearance?.position === "left" ? "left" : "right";
@@ -11794,6 +11787,7 @@ function LiveAgent(props) {
11794
11787
  const effectiveAuthToken = usesHostAuth ? authTokenProp : auth.authToken ?? undefined;
11795
11788
  const [activeModule, setActiveModule] = useState39(null);
11796
11789
  const [sessionAgentMode, setSessionAgentMode] = useState39(null);
11790
+ const [isExchanging, setIsExchanging] = useState39(false);
11797
11791
  const minimizeOnSessionStart = useCallback33(() => {
11798
11792
  if (minimizable) {
11799
11793
  setIsMinimized(true);
@@ -11804,7 +11798,10 @@ function LiveAgent(props) {
11804
11798
  setIsMinimized(false);
11805
11799
  setIsPanelOpen(true);
11806
11800
  }, []);
11801
+ const isExchangeDisconnectingRef = useRef42(false);
11807
11802
  const minimizeOnSessionDisconnect = useCallback33(() => {
11803
+ if (isExchangeDisconnectingRef.current)
11804
+ return;
11808
11805
  setActiveModule(null);
11809
11806
  setSessionAgentMode(null);
11810
11807
  if (minimizable) {
@@ -11841,14 +11838,21 @@ function LiveAgent(props) {
11841
11838
  onStartError: expandOnSessionStartError,
11842
11839
  onDisconnect: minimizeOnSessionDisconnect
11843
11840
  });
11844
- const teardownInFlightRef = useRef42(false);
11845
- teardownInFlightRef.current = isPaused || isPausing || isDisconnecting;
11841
+ const isConnected = connection !== null;
11842
+ const isSessionLive = hasLiveSession({
11843
+ isConnected,
11844
+ isStarting,
11845
+ isPausing,
11846
+ isDisconnecting,
11847
+ isExchanging
11848
+ });
11849
+ const isRoomReleasedRef = useRef42(false);
11850
+ isRoomReleasedRef.current = !isConnected || isPausing || isDisconnecting;
11846
11851
  const handleRoomDisconnected = useCallback33(() => {
11847
- if (teardownInFlightRef.current)
11852
+ if (isExchangeDisconnectingRef.current || isRoomReleasedRef.current)
11848
11853
  return;
11849
11854
  disconnect();
11850
11855
  }, [disconnect]);
11851
- const isSessionLive = connection !== null || isStarting || isPausing || isDisconnecting;
11852
11856
  const [isPanelOpen, setIsPanelOpen] = useState39(defaultOpen);
11853
11857
  const [isMinimized, setIsMinimized] = useState39(minimizable && !defaultOpen);
11854
11858
  const [pendingStart, setPendingStart] = useState39(null);
@@ -11864,7 +11868,6 @@ function LiveAgent(props) {
11864
11868
  const setTextModeState = useCallback33((enabled) => {
11865
11869
  setTextMode(enabled);
11866
11870
  }, []);
11867
- const isConnected = connection !== null;
11868
11871
  const showControlBar = isConnected || isStarting || isPausing;
11869
11872
  useEffect49(() => {
11870
11873
  if (!isConnected)
@@ -11921,19 +11924,40 @@ function LiveAgent(props) {
11921
11924
  existingSessionId: resumableSession.id
11922
11925
  });
11923
11926
  }, [resumableSession, operatingModule, startSession, baseAgentControls]);
11924
- const selectModule = useCallback33((moduleId, opts) => {
11927
+ const selectModule = useCallback33(async (moduleId, opts) => {
11928
+ if (isExchangeDisconnectingRef.current)
11929
+ return;
11925
11930
  const found = availableModules.find((m) => m.id === moduleId);
11926
11931
  if (!found)
11927
11932
  return;
11933
+ if (isSessionLive && found.id !== operatingModule?.id) {
11934
+ isExchangeDisconnectingRef.current = true;
11935
+ setIsExchanging(true);
11936
+ try {
11937
+ await disconnect();
11938
+ setActiveModule(found);
11939
+ setSessionAgentMode(found.mode);
11940
+ await startSession({
11941
+ agentId: found.id,
11942
+ agentControls: found.controls,
11943
+ existingSessionId: getResumableSessionId(found.currentSession, found.mode),
11944
+ autoStarted: opts?.autoStarted
11945
+ });
11946
+ } finally {
11947
+ isExchangeDisconnectingRef.current = false;
11948
+ setIsExchanging(false);
11949
+ }
11950
+ return;
11951
+ }
11928
11952
  setActiveModule(found);
11929
11953
  setSessionAgentMode(found.mode);
11930
- startSession({
11954
+ await startSession({
11931
11955
  agentId: found.id,
11932
11956
  agentControls: found.controls,
11933
11957
  existingSessionId: getResumableSessionId(found.currentSession, found.mode),
11934
11958
  autoStarted: opts?.autoStarted
11935
11959
  });
11936
- }, [availableModules, startSession]);
11960
+ }, [availableModules, startSession, isSessionLive, operatingModule, disconnect]);
11937
11961
  const startAdoptionSession = useCallback33((goal) => {
11938
11962
  attendGoal(goal.goalId);
11939
11963
  const found = availableModules.find((m) => m.id === goal.agentId);
@@ -12136,6 +12160,7 @@ function LiveAgent(props) {
12136
12160
  shouldConnect,
12137
12161
  isConnected,
12138
12162
  isStarting,
12163
+ isExchanging,
12139
12164
  isDisconnecting,
12140
12165
  isPausing,
12141
12166
  error,
@@ -12222,6 +12247,7 @@ function LiveAgent(props) {
12222
12247
  shouldConnect,
12223
12248
  isConnected,
12224
12249
  isStarting,
12250
+ isExchanging,
12225
12251
  isDisconnecting,
12226
12252
  isPausing,
12227
12253
  error,