@skippr/live-agent-sdk 0.34.0 → 0.35.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.
@@ -8,7 +8,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
 
9
9
  // src/components/LiveAgent.tsx
10
10
  import { LiveKitRoom, RoomAudioRenderer } from "@livekit/components-react";
11
- import { useCallback as useCallback8, useMemo as useMemo5, useState as useState11 } from "react";
11
+ import { useCallback as useCallback8, useMemo as useMemo6, useRef as useRef10, useState as useState11 } from "react";
12
12
 
13
13
  // src/context/LiveAgentContext.tsx
14
14
  import { createContext } from "react";
@@ -203,8 +203,41 @@ function useAvailableModules({
203
203
  }
204
204
 
205
205
  // src/hooks/useSession.ts
206
- import { useCallback as useCallback3, useEffect as useEffect3, useState as useState3 } from "react";
206
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef, useState as useState3 } from "react";
207
207
  var API_URL3 = "https://specialist.skippr.ai/api";
208
+ async function fetchSessionMessages(sessionId, bearerToken) {
209
+ try {
210
+ const resp = await fetch(`${API_URL3}/v1/sessions/${sessionId}/messages`, {
211
+ credentials: "omit",
212
+ headers: { Authorization: `Bearer ${bearerToken}` }
213
+ });
214
+ if (!resp.ok)
215
+ return [];
216
+ const { messages } = await resp.json();
217
+ return messages.filter((message) => message.role !== "system").map((message) => ({
218
+ id: message.id,
219
+ role: message.role === "user" ? "user" : "assistant",
220
+ content: message.content,
221
+ source: "chat",
222
+ timestamp: new Date(message.createdAt).getTime()
223
+ }));
224
+ } catch {
225
+ return [];
226
+ }
227
+ }
228
+ async function requestScreenShare() {
229
+ try {
230
+ return await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: "browser" } });
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+ function stopStream(stream) {
236
+ if (!stream)
237
+ return;
238
+ for (const track of stream.getTracks())
239
+ track.stop();
240
+ }
208
241
  async function exchangeForBearerToken(appKey, userToken) {
209
242
  const resp = await fetch(`${API_URL3}/v1/auth/token-exchange`, {
210
243
  method: "POST",
@@ -238,6 +271,10 @@ function useSession({
238
271
  const [sessionId, setSessionId] = useState3(null);
239
272
  const [bearerToken, setBearerToken] = useState3(authToken ?? null);
240
273
  const [pendingScreenStream, setPendingScreenStream] = useState3(null);
274
+ const [isPaused, setIsPaused] = useState3(false);
275
+ const [isPausing, setIsPausing] = useState3(false);
276
+ const [resumableSessions, setResumableSessions] = useState3([]);
277
+ const [historyMessages, setHistoryMessages] = useState3([]);
241
278
  useEffect3(() => {
242
279
  let stale = false;
243
280
  if (authToken) {
@@ -257,6 +294,46 @@ function useSession({
257
294
  stale = true;
258
295
  };
259
296
  }, [authToken, appKey, userToken]);
297
+ const refetchResumable = useCallback3(async () => {
298
+ if (!bearerToken) {
299
+ setResumableSessions([]);
300
+ return;
301
+ }
302
+ try {
303
+ const resp = await fetch(`${API_URL3}/v1/sessions/resumable`, {
304
+ credentials: "omit",
305
+ headers: { Authorization: `Bearer ${bearerToken}` }
306
+ });
307
+ if (!resp.ok) {
308
+ setResumableSessions([]);
309
+ return;
310
+ }
311
+ const { sessions } = await resp.json();
312
+ setResumableSessions(sessions);
313
+ } catch {
314
+ setResumableSessions([]);
315
+ }
316
+ }, [bearerToken]);
317
+ useEffect3(() => {
318
+ refetchResumable();
319
+ }, [refetchResumable]);
320
+ const pauseOnUnloadRef = useRef(null);
321
+ pauseOnUnloadRef.current = connection !== null && !isPaused && sessionId && bearerToken ? { sessionId, bearerToken } : null;
322
+ useEffect3(() => {
323
+ const onPageHide = () => {
324
+ const pending = pauseOnUnloadRef.current;
325
+ if (!pending)
326
+ return;
327
+ fetch(`${API_URL3}/v1/sessions/${pending.sessionId}/pause`, {
328
+ method: "POST",
329
+ credentials: "omit",
330
+ keepalive: true,
331
+ headers: { Authorization: `Bearer ${pending.bearerToken}` }
332
+ }).catch(() => {});
333
+ };
334
+ window.addEventListener("pagehide", onPageHide);
335
+ return () => window.removeEventListener("pagehide", onPageHide);
336
+ }, []);
260
337
  const startSession = useCallback3(async ({ agentId, agentControls }) => {
261
338
  if (!bearerToken) {
262
339
  setError("No auth token available");
@@ -272,13 +349,7 @@ function useSession({
272
349
  onStart?.();
273
350
  let screenStream = null;
274
351
  if (captureMode === "screenshare") {
275
- try {
276
- screenStream = await navigator.mediaDevices.getDisplayMedia({
277
- video: { displaySurface: "browser" }
278
- });
279
- } catch {
280
- screenStream = null;
281
- }
352
+ screenStream = await requestScreenShare();
282
353
  }
283
354
  const requestAgentControls = agentControls?.highlight === true ? { highlight: true } : undefined;
284
355
  const headers = { Authorization: `Bearer ${bearerToken}` };
@@ -311,23 +382,97 @@ function useSession({
311
382
  }
312
383
  const { connection: conn } = await startResp.json();
313
384
  setSessionId(session.id);
385
+ setHistoryMessages([]);
314
386
  setConnection({
315
387
  livekitUrl: conn.livekitUrl,
316
388
  token: conn.token
317
389
  });
318
390
  setPendingScreenStream(screenStream);
319
391
  setShouldConnect(true);
392
+ setIsPaused(false);
320
393
  } catch (e) {
321
- if (screenStream) {
322
- for (const track of screenStream.getTracks())
323
- track.stop();
324
- }
394
+ stopStream(screenStream);
325
395
  setError(e instanceof Error ? e.message : "Failed to start session");
326
396
  onStartError?.();
327
397
  } finally {
328
398
  setIsStarting(false);
329
399
  }
330
400
  }, [captureMode, bearerToken, onStart, onStartError]);
401
+ const pauseSession = useCallback3(async () => {
402
+ if (!sessionId || !bearerToken)
403
+ return;
404
+ setIsPausing(true);
405
+ try {
406
+ const resp = await fetch(`${API_URL3}/v1/sessions/${sessionId}/pause`, {
407
+ method: "POST",
408
+ credentials: "omit",
409
+ headers: { Authorization: `Bearer ${bearerToken}` }
410
+ });
411
+ if (!resp.ok) {
412
+ const body = await resp.json().catch(() => null);
413
+ throw new Error(body?.detail || `Failed to pause: ${resp.status}`);
414
+ }
415
+ } catch (e) {
416
+ setError(e instanceof Error ? e.message : "Failed to pause session");
417
+ setIsPausing(false);
418
+ return;
419
+ }
420
+ const history2 = await fetchSessionMessages(sessionId, bearerToken);
421
+ stopStream(pendingScreenStream);
422
+ setPendingScreenStream(null);
423
+ setShouldConnect(false);
424
+ setConnection(null);
425
+ setHistoryMessages(history2);
426
+ setIsPaused(true);
427
+ setIsPausing(false);
428
+ await refetchResumable();
429
+ }, [sessionId, bearerToken, pendingScreenStream, refetchResumable]);
430
+ const resumeSession = useCallback3(async ({
431
+ sessionId: resumeId,
432
+ agentControls
433
+ }) => {
434
+ if (!bearerToken) {
435
+ setError("No auth token available");
436
+ return;
437
+ }
438
+ setIsStarting(true);
439
+ setError("");
440
+ setErrorCode(null);
441
+ onStart?.();
442
+ let screenStream = null;
443
+ if (captureMode === "screenshare") {
444
+ screenStream = await requestScreenShare();
445
+ }
446
+ const requestAgentControls = agentControls?.highlight === true ? { highlight: true } : undefined;
447
+ try {
448
+ const resp = await fetch(`${API_URL3}/v1/sessions/${resumeId}/resume`, {
449
+ method: "POST",
450
+ credentials: "omit",
451
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${bearerToken}` },
452
+ body: JSON.stringify({ captureMode, agentControls: requestAgentControls })
453
+ });
454
+ if (!resp.ok) {
455
+ const body = await resp.json().catch(() => null);
456
+ setErrorCode(resp.status);
457
+ throw new Error(body?.detail || `Failed to resume: ${resp.status}`);
458
+ }
459
+ const { session, connection: conn } = await resp.json();
460
+ const history2 = await fetchSessionMessages(session.id, bearerToken);
461
+ setSessionId(session.id);
462
+ setConnection({ livekitUrl: conn.livekitUrl, token: conn.token });
463
+ setPendingScreenStream(screenStream);
464
+ setHistoryMessages(history2);
465
+ setShouldConnect(true);
466
+ setIsPaused(false);
467
+ setResumableSessions((prev) => prev.filter((s) => s.id !== resumeId));
468
+ } catch (e) {
469
+ stopStream(screenStream);
470
+ setError(e instanceof Error ? e.message : "Failed to resume session");
471
+ onStartError?.();
472
+ } finally {
473
+ setIsStarting(false);
474
+ }
475
+ }, [captureMode, bearerToken, onStart, onStartError]);
331
476
  const disconnect = useCallback3(async () => {
332
477
  setIsDisconnecting(true);
333
478
  try {
@@ -341,20 +486,20 @@ function useSession({
341
486
  });
342
487
  } catch {}
343
488
  }
344
- if (pendingScreenStream) {
345
- for (const track of pendingScreenStream.getTracks())
346
- track.stop();
347
- }
489
+ stopStream(pendingScreenStream);
348
490
  setError("");
349
491
  setShouldConnect(false);
350
492
  setConnection(null);
351
493
  setSessionId(null);
352
494
  setPendingScreenStream(null);
495
+ setHistoryMessages([]);
496
+ setIsPaused(false);
353
497
  onDisconnect?.();
498
+ await refetchResumable();
354
499
  } finally {
355
500
  setIsDisconnecting(false);
356
501
  }
357
- }, [sessionId, bearerToken, pendingScreenStream, onDisconnect]);
502
+ }, [sessionId, bearerToken, pendingScreenStream, onDisconnect, refetchResumable]);
358
503
  return {
359
504
  connection,
360
505
  shouldConnect,
@@ -363,7 +508,14 @@ function useSession({
363
508
  error,
364
509
  errorCode,
365
510
  startSession,
511
+ pauseSession,
512
+ resumeSession,
366
513
  disconnect,
514
+ isPaused,
515
+ isPausing,
516
+ resumableSessions,
517
+ historyMessages,
518
+ refetchResumable,
367
519
  pendingScreenStream,
368
520
  bearerToken
369
521
  };
@@ -382,11 +534,11 @@ var NAME_MAX_CHARS = 80;
382
534
  // src/components/AutoStartMedia.tsx
383
535
  import { useConnectionState, useLocalParticipant } from "@livekit/components-react/hooks";
384
536
  import { ConnectionState, Track } from "livekit-client";
385
- import { useEffect as useEffect4, useRef } from "react";
537
+ import { useEffect as useEffect4, useRef as useRef2 } from "react";
386
538
  function AutoStartMedia({ pendingScreenStream }) {
387
539
  const { localParticipant } = useLocalParticipant();
388
540
  const connectionState = useConnectionState();
389
- const didStartRef = useRef(false);
541
+ const didStartRef = useRef2(false);
390
542
  useEffect4(() => {
391
543
  if (didStartRef.current)
392
544
  return;
@@ -412,7 +564,7 @@ function AutoStartMedia({ pendingScreenStream }) {
412
564
  // src/components/DomCapture.tsx
413
565
  import { useConnectionState as useConnectionState2, useLocalParticipant as useLocalParticipant2 } from "@livekit/components-react/hooks";
414
566
  import { ConnectionState as ConnectionState2, ScreenSharePresets, Track as Track2 } from "livekit-client";
415
- import { useEffect as useEffect5, useRef as useRef2 } from "react";
567
+ import { useEffect as useEffect5, useRef as useRef3 } from "react";
416
568
 
417
569
  // src/capture/a11yUtils.ts
418
570
  var ROLE_BY_TAG = {
@@ -1159,7 +1311,7 @@ async function unpublishAndStopTrack(localParticipant, videoTrack) {
1159
1311
  function DomCapture() {
1160
1312
  const { localParticipant } = useLocalParticipant2();
1161
1313
  const connectionState = useConnectionState2();
1162
- const didStartRef = useRef2(false);
1314
+ const didStartRef = useRef3(false);
1163
1315
  useEffect5(() => {
1164
1316
  if (didStartRef.current)
1165
1317
  return;
@@ -1499,8 +1651,14 @@ var __iconNode18 = [
1499
1651
  ]
1500
1652
  ];
1501
1653
  var MousePointer2 = createLucideIcon("mouse-pointer-2", __iconNode18);
1502
- // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/phone-off.js
1654
+ // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/pause.js
1503
1655
  var __iconNode19 = [
1656
+ ["rect", { x: "14", y: "3", width: "5", height: "18", rx: "1", key: "kaeet6" }],
1657
+ ["rect", { x: "5", y: "3", width: "5", height: "18", rx: "1", key: "1wsw3u" }]
1658
+ ];
1659
+ var Pause = createLucideIcon("pause", __iconNode19);
1660
+ // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/phone-off.js
1661
+ var __iconNode20 = [
1504
1662
  [
1505
1663
  "path",
1506
1664
  {
@@ -1517,9 +1675,20 @@ var __iconNode19 = [
1517
1675
  }
1518
1676
  ]
1519
1677
  ];
1520
- var PhoneOff = createLucideIcon("phone-off", __iconNode19);
1678
+ var PhoneOff = createLucideIcon("phone-off", __iconNode20);
1679
+ // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/play.js
1680
+ var __iconNode21 = [
1681
+ [
1682
+ "path",
1683
+ {
1684
+ d: "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z",
1685
+ key: "10ikf1"
1686
+ }
1687
+ ]
1688
+ ];
1689
+ var Play = createLucideIcon("play", __iconNode21);
1521
1690
  // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/rocket.js
1522
- var __iconNode20 = [
1691
+ var __iconNode22 = [
1523
1692
  ["path", { d: "M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5", key: "qeys4" }],
1524
1693
  [
1525
1694
  "path",
@@ -1537,9 +1706,9 @@ var __iconNode20 = [
1537
1706
  ],
1538
1707
  ["path", { d: "M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05", key: "92ym6u" }]
1539
1708
  ];
1540
- var Rocket = createLucideIcon("rocket", __iconNode20);
1709
+ var Rocket = createLucideIcon("rocket", __iconNode22);
1541
1710
  // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/send.js
1542
- var __iconNode21 = [
1711
+ var __iconNode23 = [
1543
1712
  [
1544
1713
  "path",
1545
1714
  {
@@ -1549,17 +1718,17 @@ var __iconNode21 = [
1549
1718
  ],
1550
1719
  ["path", { d: "m21.854 2.147-10.94 10.939", key: "12cjpa" }]
1551
1720
  ];
1552
- var Send = createLucideIcon("send", __iconNode21);
1721
+ var Send = createLucideIcon("send", __iconNode23);
1553
1722
  // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/user-plus.js
1554
- var __iconNode22 = [
1723
+ var __iconNode24 = [
1555
1724
  ["path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2", key: "1yyitq" }],
1556
1725
  ["circle", { cx: "9", cy: "7", r: "4", key: "nufk8" }],
1557
1726
  ["line", { x1: "19", x2: "19", y1: "8", y2: "14", key: "1bvyxn" }],
1558
1727
  ["line", { x1: "22", x2: "16", y1: "11", y2: "11", key: "1shjgl" }]
1559
1728
  ];
1560
- var UserPlus = createLucideIcon("user-plus", __iconNode22);
1729
+ var UserPlus = createLucideIcon("user-plus", __iconNode24);
1561
1730
  // src/components/HighlightOverlay.tsx
1562
- import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef3, useState as useState4 } from "react";
1731
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef4, useState as useState4 } from "react";
1563
1732
  import { jsx, jsxs } from "react/jsx-runtime";
1564
1733
  var Z_INDEX = 2147483646;
1565
1734
  var HIGHLIGHT_PADDING = 6;
@@ -1664,8 +1833,8 @@ function findScrollableAncestor(el) {
1664
1833
  }
1665
1834
  function HighlightOverlay() {
1666
1835
  const [overlayState, setOverlayState] = useState4(null);
1667
- const targetElementRef = useRef3(null);
1668
- const pendingFrameRef = useRef3(null);
1836
+ const targetElementRef = useRef4(null);
1837
+ const pendingFrameRef = useRef4(null);
1669
1838
  const clearOverlay = useCallback4(() => {
1670
1839
  targetElementRef.current = null;
1671
1840
  setOverlayState(null);
@@ -1792,14 +1961,27 @@ function HighlightOverlay() {
1792
1961
  // src/components/MinimizedBubble.tsx
1793
1962
  import { useEffect as useEffect7 } from "react";
1794
1963
 
1964
+ // src/lib/react-compat.ts
1965
+ import * as React from "react";
1966
+ function selectContextHook(use2, useContext3) {
1967
+ return use2 ?? useContext3;
1968
+ }
1969
+ var useContextValue = selectContextHook(React.use, React.useContext);
1970
+
1795
1971
  // src/hooks/useLiveAgent.ts
1796
- import { use } from "react";
1797
1972
  function useLiveAgent() {
1798
- const ctx = use(LiveAgentContext);
1973
+ const ctx = useContextValue(LiveAgentContext);
1799
1974
  if (!ctx) {
1800
1975
  throw new Error("useLiveAgent must be used within a <LiveAgent> provider");
1801
1976
  }
1802
- const { connection, shouldConnect, ...publicValue } = ctx;
1977
+ const {
1978
+ connection,
1979
+ shouldConnect,
1980
+ historyMessages,
1981
+ phasesSnapshot,
1982
+ setPhasesSnapshot,
1983
+ ...publicValue
1984
+ } = ctx;
1803
1985
  return publicValue;
1804
1986
  }
1805
1987
 
@@ -1861,14 +2043,29 @@ function pillStatusFromAgent(state, canSeePage) {
1861
2043
  return canSeePage ? "observing" : "connected";
1862
2044
  }
1863
2045
  function LauncherStatusPill() {
1864
- const { isConnected, isStarting, expandPanel, setSidebarTab, position, captureMode } = useLiveAgent();
2046
+ const { isConnected, isStarting, isPaused, expandPanel, setSidebarTab, position, captureMode } = useLiveAgent();
1865
2047
  const { state } = useAgentVoiceState();
1866
2048
  const { isScreenSharing } = useMediaControls();
1867
2049
  const isStandingBy = isStarting && !isConnected;
1868
- if (!isConnected && !isStandingBy)
2050
+ if (!isConnected && !isStandingBy && !isPaused)
1869
2051
  return null;
1870
2052
  const canSeePage = captureMode === "auto" || isScreenSharing;
1871
- const status = isStandingBy ? "standing-by" : pillStatusFromAgent(state, canSeePage);
2053
+ let status;
2054
+ if (isPaused) {
2055
+ status = "paused";
2056
+ } else if (isStandingBy) {
2057
+ status = "standing-by";
2058
+ } else {
2059
+ status = pillStatusFromAgent(state, canSeePage);
2060
+ }
2061
+ let ariaLabel;
2062
+ if (isStandingBy) {
2063
+ ariaLabel = "Skippr is standing by — click to open";
2064
+ } else if (isPaused) {
2065
+ ariaLabel = "Session paused — click to open";
2066
+ } else {
2067
+ ariaLabel = `Skippr is ${status} — click to open chat`;
2068
+ }
1872
2069
  const handleClick = () => {
1873
2070
  if (!isStandingBy)
1874
2071
  setSidebarTab("chat");
@@ -1878,7 +2075,7 @@ function LauncherStatusPill() {
1878
2075
  type: "button",
1879
2076
  onClick: handleClick,
1880
2077
  className: cn("skippr:fixed skippr:bottom-20 skippr:z-[9999]", "skippr:flex skippr:items-center skippr:gap-2", "skippr:rounded-full skippr:bg-bubble/95 skippr:backdrop-blur-sm", "skippr:px-3 skippr:py-1.5", "skippr:text-xs skippr:font-medium skippr:text-white", "skippr:shadow-[0_8px_24px_rgba(45,43,61,0.35)]", "skippr:cursor-pointer skippr:transition-colors skippr:hover:bg-bubble", "skippr:animate-[skippr-bubble-in_0.28s_ease-out]", position === "right" ? "skippr:right-6" : "skippr:left-6"),
1881
- "aria-label": isStandingBy ? "Skippr is standing by — click to open" : `Skippr is ${status} — click to open chat`,
2078
+ "aria-label": ariaLabel,
1882
2079
  children: /* @__PURE__ */ jsxs2("span", {
1883
2080
  className: "skippr:flex skippr:items-center skippr:gap-2 skippr:animate-[skippr-pill-content_0.22s_ease-out]",
1884
2081
  children: [
@@ -1921,6 +2118,14 @@ function LauncherStatusPill() {
1921
2118
  children: "Skippr is thinking"
1922
2119
  })
1923
2120
  ]
2121
+ }),
2122
+ status === "paused" && /* @__PURE__ */ jsxs2(Fragment, {
2123
+ children: [
2124
+ /* @__PURE__ */ jsx2(PausedDot, {}),
2125
+ /* @__PURE__ */ jsx2("span", {
2126
+ children: "Session paused"
2127
+ })
2128
+ ]
1924
2129
  })
1925
2130
  ]
1926
2131
  }, status)
@@ -1939,6 +2144,11 @@ function ConnectedDot() {
1939
2144
  ]
1940
2145
  });
1941
2146
  }
2147
+ function PausedDot() {
2148
+ return /* @__PURE__ */ jsx2("span", {
2149
+ className: "skippr:inline-flex skippr:size-2 skippr:rounded-full skippr:bg-amber-400"
2150
+ });
2151
+ }
1942
2152
  function ObservingIcon() {
1943
2153
  return /* @__PURE__ */ jsxs2("svg", {
1944
2154
  viewBox: "0 0 24 24",
@@ -2090,34 +2300,60 @@ import { jsx as jsx4, jsxs as jsxs4, Fragment as Fragment2 } from "react/jsx-run
2090
2300
  var CONTROL_BUTTON = "skippr:flex skippr:size-12 skippr:items-center skippr:justify-center skippr:rounded-[14px] skippr:cursor-pointer skippr:transition-all skippr:hover:-translate-y-0.5 skippr:active:translate-y-0";
2091
2301
  var CONTROL_SHADOW = "skippr:shadow-[0_4px_16px_rgba(0,0,0,0.15),0_2px_4px_rgba(0,0,0,0.1)]";
2092
2302
  function ConnectedLauncher() {
2093
- const { expandPanel, disconnect, captureMode, setSidebarTab } = useLiveAgent();
2303
+ const {
2304
+ expandPanel,
2305
+ disconnect,
2306
+ pauseSession,
2307
+ resumeSession,
2308
+ isPaused,
2309
+ isPausing,
2310
+ captureMode,
2311
+ setSidebarTab
2312
+ } = useLiveAgent();
2094
2313
  const { isMuted, toggleMute, isScreenSharing, toggleScreenShare } = useMediaControls();
2095
2314
  const showScreenShareToggle = captureMode === "screenshare";
2315
+ const showPaused = isPaused || isPausing;
2096
2316
  const openChat = () => {
2097
2317
  setSidebarTab("chat");
2098
2318
  expandPanel();
2099
2319
  };
2100
2320
  return /* @__PURE__ */ jsxs4(Fragment2, {
2101
2321
  children: [
2102
- /* @__PURE__ */ jsx4("button", {
2103
- type: "button",
2104
- onClick: toggleMute,
2105
- "aria-label": isMuted ? "Unmute" : "Mute",
2106
- className: cn(CONTROL_BUTTON, CONTROL_SHADOW, isMuted ? "skippr:bg-destructive/10 skippr:text-destructive skippr:hover:bg-destructive/20" : "skippr:bg-white skippr:text-foreground skippr:hover:bg-muted"),
2107
- children: isMuted ? /* @__PURE__ */ jsx4(MicOff, {
2108
- className: "skippr:size-5"
2109
- }) : /* @__PURE__ */ jsx4(Mic, {
2110
- className: "skippr:size-5"
2111
- })
2322
+ !showPaused && /* @__PURE__ */ jsxs4(Fragment2, {
2323
+ children: [
2324
+ /* @__PURE__ */ jsx4("button", {
2325
+ type: "button",
2326
+ onClick: toggleMute,
2327
+ "aria-label": isMuted ? "Unmute" : "Mute",
2328
+ className: cn(CONTROL_BUTTON, CONTROL_SHADOW, isMuted ? "skippr:bg-destructive/10 skippr:text-destructive skippr:hover:bg-destructive/20" : "skippr:bg-white skippr:text-foreground skippr:hover:bg-muted"),
2329
+ children: isMuted ? /* @__PURE__ */ jsx4(MicOff, {
2330
+ className: "skippr:size-5"
2331
+ }) : /* @__PURE__ */ jsx4(Mic, {
2332
+ className: "skippr:size-5"
2333
+ })
2334
+ }),
2335
+ showScreenShareToggle && /* @__PURE__ */ jsx4("button", {
2336
+ type: "button",
2337
+ onClick: toggleScreenShare,
2338
+ "aria-label": isScreenSharing ? "Stop sharing screen" : "Share screen",
2339
+ className: cn(CONTROL_BUTTON, CONTROL_SHADOW, isScreenSharing ? "skippr:bg-primary skippr:text-primary-foreground skippr:hover:bg-primary/90" : "skippr:bg-white skippr:text-foreground skippr:hover:bg-muted"),
2340
+ children: isScreenSharing ? /* @__PURE__ */ jsx4(MonitorOff, {
2341
+ className: "skippr:size-5"
2342
+ }) : /* @__PURE__ */ jsx4(Monitor, {
2343
+ className: "skippr:size-5"
2344
+ })
2345
+ })
2346
+ ]
2112
2347
  }),
2113
- showScreenShareToggle && /* @__PURE__ */ jsx4("button", {
2348
+ /* @__PURE__ */ jsx4("button", {
2114
2349
  type: "button",
2115
- onClick: toggleScreenShare,
2116
- "aria-label": isScreenSharing ? "Stop sharing screen" : "Share screen",
2117
- className: cn(CONTROL_BUTTON, CONTROL_SHADOW, isScreenSharing ? "skippr:bg-primary skippr:text-primary-foreground skippr:hover:bg-primary/90" : "skippr:bg-white skippr:text-foreground skippr:hover:bg-muted"),
2118
- children: isScreenSharing ? /* @__PURE__ */ jsx4(MonitorOff, {
2350
+ onClick: () => isPaused ? resumeSession() : pauseSession(),
2351
+ disabled: isPausing,
2352
+ "aria-label": showPaused ? "Resume session" : "Pause session",
2353
+ className: cn(CONTROL_BUTTON, CONTROL_SHADOW, "skippr:bg-white skippr:text-foreground skippr:hover:bg-muted skippr:disabled:opacity-60"),
2354
+ children: showPaused ? /* @__PURE__ */ jsx4(Play, {
2119
2355
  className: "skippr:size-5"
2120
- }) : /* @__PURE__ */ jsx4(Monitor, {
2356
+ }) : /* @__PURE__ */ jsx4(Pause, {
2121
2357
  className: "skippr:size-5"
2122
2358
  })
2123
2359
  }),
@@ -2130,7 +2366,7 @@ function ConnectedLauncher() {
2130
2366
  className: "skippr:size-5"
2131
2367
  })
2132
2368
  }),
2133
- /* @__PURE__ */ jsx4("button", {
2369
+ !showPaused && /* @__PURE__ */ jsx4("button", {
2134
2370
  type: "button",
2135
2371
  onClick: openChat,
2136
2372
  "aria-label": "Open chat",
@@ -2181,8 +2417,8 @@ function MinimizedBubble({
2181
2417
  welcomeDismissed,
2182
2418
  onDismissWelcome
2183
2419
  }) {
2184
- const { isConnected, isStarting, position } = useLiveAgent();
2185
- const inSession = isConnected;
2420
+ const { isConnected, isStarting, isPaused, isPausing, position } = useLiveAgent();
2421
+ const inSession = isConnected || isPaused || isPausing;
2186
2422
  return /* @__PURE__ */ jsxs4(Fragment2, {
2187
2423
  children: [
2188
2424
  /* @__PURE__ */ jsx4(LauncherStatusPill, {}),
@@ -2202,7 +2438,7 @@ function MinimizedBubble({
2202
2438
  }
2203
2439
 
2204
2440
  // src/components/Sidebar.tsx
2205
- import { useEffect as useEffect15 } from "react";
2441
+ import { useEffect as useEffect16 } from "react";
2206
2442
 
2207
2443
  // src/hooks/useCombinedMessages.ts
2208
2444
  import { useMemo as useMemo4 } from "react";
@@ -2273,18 +2509,25 @@ function useCombinedMessages() {
2273
2509
  const { transcriptMessages } = useStreamingTranscript();
2274
2510
  const { chatMessages, sendChatMessage, isSendingChat } = useChatMessages();
2275
2511
  const { state: agentState } = useAgentVoiceState();
2276
- const allMessages = useMemo4(() => {
2512
+ const historyMessages = useContextValue(LiveAgentContext)?.historyMessages ?? [];
2513
+ const liveMessages = useMemo4(() => {
2277
2514
  if (chatMessages.length === 0)
2278
2515
  return transcriptMessages;
2279
2516
  if (transcriptMessages.length === 0)
2280
2517
  return chatMessages;
2281
2518
  return mergeChatsIntoTranscripts(transcriptMessages, chatMessages);
2282
2519
  }, [transcriptMessages, chatMessages]);
2520
+ const allMessages = useMemo4(() => {
2521
+ if (historyMessages.length === 0)
2522
+ return liveMessages;
2523
+ const seenIds = new Set(liveMessages.map((message) => message.id));
2524
+ return [...historyMessages.filter((message) => !seenIds.has(message.id)), ...liveMessages];
2525
+ }, [historyMessages, liveMessages]);
2283
2526
  return { allMessages, agentState, sendChatMessage, isSendingChat };
2284
2527
  }
2285
2528
 
2286
2529
  // src/hooks/usePhaseUpdates.ts
2287
- import { useCallback as useCallback6 } from "react";
2530
+ import { useCallback as useCallback6, useEffect as useEffect9 } from "react";
2288
2531
 
2289
2532
  // src/hooks/useAgentState.ts
2290
2533
  import { useRemoteParticipants } from "@livekit/components-react/hooks";
@@ -2339,12 +2582,18 @@ function parsePhases(json) {
2339
2582
  }
2340
2583
  function usePhaseUpdates() {
2341
2584
  const parse = useCallback6(parsePhases, []);
2342
- const phases = useAgentState("phases", parse, []);
2585
+ const livePhases = useAgentState("phases", parse, []);
2586
+ const ctx = useContextValue(LiveAgentContext);
2587
+ useEffect9(() => {
2588
+ if (livePhases.length > 0)
2589
+ ctx?.setPhasesSnapshot(livePhases);
2590
+ }, [livePhases, ctx?.setPhasesSnapshot]);
2591
+ const phases = livePhases.length > 0 ? livePhases : ctx?.phasesSnapshot ?? [];
2343
2592
  return { phases };
2344
2593
  }
2345
2594
 
2346
2595
  // src/hooks/useSessionRemaining.ts
2347
- import { useEffect as useEffect9, useRef as useRef4, useState as useState6 } from "react";
2596
+ import { useEffect as useEffect10, useRef as useRef5, useState as useState6 } from "react";
2348
2597
 
2349
2598
  // src/lib/format.ts
2350
2599
  function formatTime(seconds) {
@@ -2360,9 +2609,9 @@ function parseNumber(s) {
2360
2609
  // src/hooks/useSessionRemaining.ts
2361
2610
  function useSessionRemaining() {
2362
2611
  const maxCallDuration = useAgentState("maxCallDuration", parseNumber, null);
2363
- const endTimeRef = useRef4(null);
2612
+ const endTimeRef = useRef5(null);
2364
2613
  const [remaining, setRemaining] = useState6(null);
2365
- useEffect9(() => {
2614
+ useEffect10(() => {
2366
2615
  if (maxCallDuration === null || endTimeRef.current !== null)
2367
2616
  return;
2368
2617
  const endTime = Date.now() + maxCallDuration * 1000;
@@ -2379,10 +2628,10 @@ function useSessionRemaining() {
2379
2628
  }
2380
2629
 
2381
2630
  // src/hooks/useElapsedSeconds.ts
2382
- import { useEffect as useEffect10, useState as useState7 } from "react";
2631
+ import { useEffect as useEffect11, useState as useState7 } from "react";
2383
2632
  function useElapsedSeconds(isRunning) {
2384
2633
  const [elapsed, setElapsed] = useState7(0);
2385
- useEffect10(() => {
2634
+ useEffect11(() => {
2386
2635
  if (!isRunning) {
2387
2636
  setElapsed(0);
2388
2637
  return;
@@ -2480,7 +2729,7 @@ function LoadingDots({ label }) {
2480
2729
  }
2481
2730
 
2482
2731
  // src/components/LoginFlow.tsx
2483
- import { useCallback as useCallback7, useEffect as useEffect11, useRef as useRef5, useState as useState8 } from "react";
2732
+ import { useCallback as useCallback7, useEffect as useEffect12, useRef as useRef6, useState as useState8 } from "react";
2484
2733
 
2485
2734
  // src/components/ui/button.tsx
2486
2735
  import { forwardRef as forwardRef3 } from "react";
@@ -2608,16 +2857,16 @@ function EmailStep({ email, onEmailChange, onSubmit, error, isSubmitting }) {
2608
2857
  function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
2609
2858
  const [digits, setDigits] = useState8(Array(OTP_LENGTH).fill(""));
2610
2859
  const [resendCooldown, setResendCooldown] = useState8(0);
2611
- const inputRefs = useRef5([]);
2612
- const submittedRef = useRef5(false);
2613
- useEffect11(() => {
2860
+ const inputRefs = useRef6([]);
2861
+ const submittedRef = useRef6(false);
2862
+ useEffect12(() => {
2614
2863
  inputRefs.current[0]?.focus();
2615
2864
  }, []);
2616
- useEffect11(() => {
2865
+ useEffect12(() => {
2617
2866
  if (error)
2618
2867
  submittedRef.current = false;
2619
2868
  }, [error]);
2620
- useEffect11(() => {
2869
+ useEffect12(() => {
2621
2870
  if (resendCooldown <= 0)
2622
2871
  return;
2623
2872
  const timer = setTimeout(() => setResendCooldown((c) => c - 1), 1000);
@@ -2758,35 +3007,66 @@ function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
2758
3007
  }
2759
3008
 
2760
3009
  // src/components/MeetingControls.tsx
2761
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2762
- var CONTROL_BUTTON2 = "skippr:flex skippr:size-11 skippr:cursor-pointer skippr:items-center skippr:justify-center skippr:rounded-full skippr:transition-colors";
2763
- function MeetingControls({ onHangUp, showScreenShareToggle = true }) {
3010
+ import { jsx as jsx9, jsxs as jsxs8, Fragment as Fragment3 } from "react/jsx-runtime";
3011
+ var CONTROL_BUTTON2 = "skippr:flex skippr:size-11 skippr:cursor-pointer skippr:items-center skippr:justify-center skippr:rounded-full skippr:transition-colors skippr:disabled:cursor-not-allowed skippr:disabled:opacity-60";
3012
+ var MUTED_BUTTON = "skippr:bg-muted skippr:text-foreground skippr:hover:bg-muted/80";
3013
+ function MeetingControls({
3014
+ onHangUp,
3015
+ onPause,
3016
+ onResume,
3017
+ isPaused = false,
3018
+ isPausing = false,
3019
+ showScreenShareToggle = true
3020
+ }) {
2764
3021
  const { isMuted, toggleMute, isScreenSharing, toggleScreenShare } = useMediaControls();
3022
+ const showPaused = isPaused || isPausing;
2765
3023
  return /* @__PURE__ */ jsxs8("div", {
2766
3024
  className: "skippr:shrink-0 skippr:border-t skippr:border-border skippr:bg-background skippr:px-4 skippr:py-4",
2767
3025
  children: [
2768
3026
  /* @__PURE__ */ jsxs8("div", {
2769
3027
  className: "skippr:flex skippr:items-center skippr:justify-center skippr:gap-3",
2770
3028
  children: [
2771
- /* @__PURE__ */ jsx9("button", {
3029
+ !showPaused && /* @__PURE__ */ jsxs8(Fragment3, {
3030
+ children: [
3031
+ /* @__PURE__ */ jsx9("button", {
3032
+ type: "button",
3033
+ onClick: toggleMute,
3034
+ "aria-label": isMuted ? "Unmute" : "Mute",
3035
+ className: cn(CONTROL_BUTTON2, isMuted ? "skippr:bg-destructive/15 skippr:text-destructive skippr:hover:bg-destructive/25" : MUTED_BUTTON),
3036
+ children: isMuted ? /* @__PURE__ */ jsx9(MicOff, {
3037
+ className: "skippr:size-5"
3038
+ }) : /* @__PURE__ */ jsx9(Mic, {
3039
+ className: "skippr:size-5"
3040
+ })
3041
+ }),
3042
+ showScreenShareToggle && /* @__PURE__ */ jsx9("button", {
3043
+ type: "button",
3044
+ onClick: toggleScreenShare,
3045
+ "aria-label": isScreenSharing ? "Stop sharing screen" : "Share screen",
3046
+ className: cn(CONTROL_BUTTON2, isScreenSharing ? "skippr:bg-bubble skippr:text-white skippr:hover:brightness-110" : MUTED_BUTTON),
3047
+ children: isScreenSharing ? /* @__PURE__ */ jsx9(MonitorOff, {
3048
+ className: "skippr:size-5"
3049
+ }) : /* @__PURE__ */ jsx9(Monitor, {
3050
+ className: "skippr:size-5"
3051
+ })
3052
+ })
3053
+ ]
3054
+ }),
3055
+ showPaused ? onResume && /* @__PURE__ */ jsx9("button", {
2772
3056
  type: "button",
2773
- onClick: toggleMute,
2774
- "aria-label": isMuted ? "Unmute" : "Mute",
2775
- className: cn(CONTROL_BUTTON2, isMuted ? "skippr:bg-destructive/15 skippr:text-destructive skippr:hover:bg-destructive/25" : "skippr:bg-muted skippr:text-foreground skippr:hover:bg-muted/80"),
2776
- children: isMuted ? /* @__PURE__ */ jsx9(MicOff, {
2777
- className: "skippr:size-5"
2778
- }) : /* @__PURE__ */ jsx9(Mic, {
3057
+ onClick: onResume,
3058
+ disabled: isPausing,
3059
+ "aria-label": "Resume session",
3060
+ className: cn(CONTROL_BUTTON2, MUTED_BUTTON),
3061
+ children: /* @__PURE__ */ jsx9(Play, {
2779
3062
  className: "skippr:size-5"
2780
3063
  })
2781
- }),
2782
- showScreenShareToggle && /* @__PURE__ */ jsx9("button", {
3064
+ }) : onPause && /* @__PURE__ */ jsx9("button", {
2783
3065
  type: "button",
2784
- onClick: toggleScreenShare,
2785
- "aria-label": isScreenSharing ? "Stop sharing screen" : "Share screen",
2786
- className: cn(CONTROL_BUTTON2, isScreenSharing ? "skippr:bg-bubble skippr:text-white skippr:hover:brightness-110" : "skippr:bg-muted skippr:text-foreground skippr:hover:bg-muted/80"),
2787
- children: isScreenSharing ? /* @__PURE__ */ jsx9(MonitorOff, {
2788
- className: "skippr:size-5"
2789
- }) : /* @__PURE__ */ jsx9(Monitor, {
3066
+ onClick: onPause,
3067
+ "aria-label": "Pause session",
3068
+ className: cn(CONTROL_BUTTON2, MUTED_BUTTON),
3069
+ children: /* @__PURE__ */ jsx9(Pause, {
2790
3070
  className: "skippr:size-5"
2791
3071
  })
2792
3072
  }),
@@ -2810,17 +3090,17 @@ function MeetingControls({ onHangUp, showScreenShareToggle = true }) {
2810
3090
  }
2811
3091
 
2812
3092
  // src/components/MessageList.tsx
2813
- import { useEffect as useEffect13, useRef as useRef7 } from "react";
3093
+ import { useEffect as useEffect14, useRef as useRef8 } from "react";
2814
3094
 
2815
3095
  // src/components/ChatInput.tsx
2816
- import { useEffect as useEffect12, useRef as useRef6, useState as useState9 } from "react";
3096
+ import { useEffect as useEffect13, useRef as useRef7, useState as useState9 } from "react";
2817
3097
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2818
3098
  var MAX_INPUT_HEIGHT = 60;
2819
3099
  function ChatInput({ sendChatMessage, isSendingChat, autoFocus = false }) {
2820
3100
  const [inputText, setInputText] = useState9("");
2821
- const textareaRef = useRef6(null);
3101
+ const textareaRef = useRef7(null);
2822
3102
  const canSend = inputText.trim().length > 0 && !isSendingChat;
2823
- useEffect12(() => {
3103
+ useEffect13(() => {
2824
3104
  if (autoFocus)
2825
3105
  textareaRef.current?.focus();
2826
3106
  }, [autoFocus]);
@@ -2965,9 +3245,9 @@ function MessageList({
2965
3245
  isSendingChat,
2966
3246
  autoFocus = false
2967
3247
  }) {
2968
- const scrollRef = useRef7(null);
3248
+ const scrollRef = useRef8(null);
2969
3249
  const lastMessage = messages.length > 0 ? messages[messages.length - 1] : undefined;
2970
- useEffect13(() => {
3250
+ useEffect14(() => {
2971
3251
  scrollRef.current?.scrollIntoView({ behavior: "smooth" });
2972
3252
  }, [messages.length, lastMessage?.content]);
2973
3253
  const showTyping = isStreaming && lastMessage?.role === "assistant" && lastMessage.content === "";
@@ -2999,7 +3279,7 @@ function MessageList({
2999
3279
  }
3000
3280
 
3001
3281
  // src/components/ModuleSelector.tsx
3002
- import { useEffect as useEffect14, useRef as useRef8, useState as useState10 } from "react";
3282
+ import { useEffect as useEffect15, useMemo as useMemo5, useRef as useRef9, useState as useState10 } from "react";
3003
3283
  import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
3004
3284
  var AGENT_TYPE_ICONS = {
3005
3285
  onboarding: UserPlus,
@@ -3019,13 +3299,15 @@ function ModuleSelector() {
3019
3299
  selectModule,
3020
3300
  isStarting,
3021
3301
  isDisconnecting,
3022
- error
3302
+ error,
3303
+ resumableSessions
3023
3304
  } = useLiveAgent();
3024
3305
  const isBusy = isStarting || isDisconnecting;
3025
- const scrollRef = useRef8(null);
3306
+ const resumableAgentIds = useMemo5(() => new Set(resumableSessions.map((s) => s.agentId)), [resumableSessions]);
3307
+ const scrollRef = useRef9(null);
3026
3308
  const [showScrollHint, setShowScrollHint] = useState10(false);
3027
3309
  const [isScrolled, setIsScrolled] = useState10(false);
3028
- useEffect14(() => {
3310
+ useEffect15(() => {
3029
3311
  const el = scrollRef.current;
3030
3312
  if (!el)
3031
3313
  return;
@@ -3109,6 +3391,7 @@ function ModuleSelector() {
3109
3391
  const isFeatured = index2 === 0;
3110
3392
  const isWide = index2 === availableModules.length - 1 && availableModules.length % 2 === 1;
3111
3393
  const Icon2 = getAgentIcon(module.type);
3394
+ const canResume = resumableAgentIds.has(module.id);
3112
3395
  const base = "skippr:group skippr:flex skippr:cursor-pointer skippr:gap-3 skippr:rounded-xl skippr:text-left skippr:transition-colors skippr:disabled:cursor-not-allowed skippr:disabled:opacity-50";
3113
3396
  const layout = isWide ? "skippr:items-center skippr:p-3.5 skippr:pb-5" : "skippr:flex-col skippr:items-start skippr:p-3.5";
3114
3397
  const variant = isFeatured ? "skippr:bg-primary skippr:text-primary-foreground skippr:hover:bg-primary/90" : "skippr:bg-background skippr:ring-1 skippr:ring-foreground/10 skippr:hover:bg-muted/50";
@@ -3128,9 +3411,23 @@ function ModuleSelector() {
3128
3411
  /* @__PURE__ */ jsxs13("div", {
3129
3412
  className: "skippr:min-w-0 skippr:w-full skippr:space-y-0.5",
3130
3413
  children: [
3131
- /* @__PURE__ */ jsx14("p", {
3132
- className: "skippr:line-clamp-1 skippr:text-sm skippr:font-semibold",
3133
- children: module.name
3414
+ /* @__PURE__ */ jsxs13("div", {
3415
+ className: "skippr:flex skippr:items-center skippr:gap-1.5",
3416
+ children: [
3417
+ /* @__PURE__ */ jsx14("p", {
3418
+ className: "skippr:line-clamp-1 skippr:text-sm skippr:font-semibold",
3419
+ children: module.name
3420
+ }),
3421
+ canResume && /* @__PURE__ */ jsxs13("span", {
3422
+ className: `skippr:inline-flex skippr:shrink-0 skippr:items-center skippr:gap-0.5 skippr:rounded-full skippr:px-1.5 skippr:py-0.5 skippr:text-[9px] skippr:font-medium skippr:uppercase skippr:tracking-wide ${isFeatured ? "skippr:bg-primary-foreground/20 skippr:text-primary-foreground" : "skippr:bg-bubble/15 skippr:text-bubble"}`,
3423
+ children: [
3424
+ /* @__PURE__ */ jsx14(Play, {
3425
+ className: "skippr:size-2.5"
3426
+ }),
3427
+ "Resume"
3428
+ ]
3429
+ })
3430
+ ]
3134
3431
  }),
3135
3432
  module.description && /* @__PURE__ */ jsx14("p", {
3136
3433
  className: isFeatured ? "skippr:line-clamp-2 skippr:text-[11px] skippr:leading-snug skippr:text-primary-foreground/70" : "skippr:line-clamp-2 skippr:text-[11px] skippr:leading-snug skippr:text-muted-foreground",
@@ -3227,18 +3524,27 @@ function SessionWarningBanner({ remaining }) {
3227
3524
 
3228
3525
  // src/components/StartSessionPrompt.tsx
3229
3526
  import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
3527
+ var PROMPT_BUTTON = "skippr:cursor-pointer skippr:rounded-xl skippr:bg-primary skippr:px-8 skippr:py-3 skippr:text-sm skippr:font-medium skippr:text-primary-foreground skippr:transition-all skippr:hover:bg-primary/90 skippr:disabled:cursor-not-allowed skippr:disabled:opacity-60";
3230
3528
  function StartSessionPrompt({
3231
3529
  onStartSession,
3232
3530
  agentId,
3233
3531
  agentControls,
3234
3532
  isStarting,
3235
3533
  error,
3236
- label = "Talk to Skippr"
3534
+ label = "Talk to Skippr",
3535
+ canResume = false,
3536
+ onResume
3237
3537
  }) {
3238
3538
  return /* @__PURE__ */ jsxs15("div", {
3239
3539
  className: "skippr:flex skippr:flex-1 skippr:flex-col skippr:items-center skippr:justify-center skippr:gap-3 skippr:px-4",
3240
3540
  children: [
3241
- /* @__PURE__ */ jsx17("button", {
3541
+ canResume && onResume ? /* @__PURE__ */ jsx17("button", {
3542
+ type: "button",
3543
+ onClick: onResume,
3544
+ disabled: isStarting,
3545
+ className: PROMPT_BUTTON,
3546
+ children: isStarting ? "Resuming..." : "Resume session"
3547
+ }) : /* @__PURE__ */ jsx17("button", {
3242
3548
  type: "button",
3243
3549
  onClick: () => {
3244
3550
  if (!agentId)
@@ -3246,7 +3552,7 @@ function StartSessionPrompt({
3246
3552
  onStartSession(agentControls ? { agentId, agentControls } : { agentId });
3247
3553
  },
3248
3554
  disabled: isStarting || !agentId,
3249
- className: "skippr:cursor-pointer skippr:rounded-xl skippr:bg-primary skippr:px-8 skippr:py-3 skippr:text-sm skippr:font-medium skippr:text-primary-foreground skippr:transition-all skippr:hover:bg-primary/90 skippr:disabled:cursor-not-allowed skippr:disabled:opacity-60",
3555
+ className: PROMPT_BUTTON,
3250
3556
  children: isStarting ? "Starting..." : label
3251
3557
  }),
3252
3558
  error && /* @__PURE__ */ jsx17("p", {
@@ -3258,7 +3564,7 @@ function StartSessionPrompt({
3258
3564
  }
3259
3565
 
3260
3566
  // src/components/Sidebar.tsx
3261
- import { jsx as jsx18, jsxs as jsxs16, Fragment as Fragment3 } from "react/jsx-runtime";
3567
+ import { jsx as jsx18, jsxs as jsxs16, Fragment as Fragment4 } from "react/jsx-runtime";
3262
3568
  function Sidebar({
3263
3569
  hideControls = false,
3264
3570
  hideHeader = false,
@@ -3268,8 +3574,14 @@ function Sidebar({
3268
3574
  variant,
3269
3575
  isConnected,
3270
3576
  isStarting,
3577
+ isDisconnecting,
3578
+ isPausing,
3579
+ isPaused,
3271
3580
  error,
3272
3581
  startSession,
3582
+ pauseSession,
3583
+ resumeSession,
3584
+ resumableSession,
3273
3585
  disconnect,
3274
3586
  isPanelOpen,
3275
3587
  position,
@@ -3289,7 +3601,7 @@ function Sidebar({
3289
3601
  } = useLiveAgent();
3290
3602
  const isFloating = variant === "floating";
3291
3603
  const isSidebar = variant === "sidebar";
3292
- useEffect15(() => {
3604
+ useEffect16(() => {
3293
3605
  if (!isSidebar)
3294
3606
  return;
3295
3607
  const prop = position === "right" ? "marginRight" : "marginLeft";
@@ -3320,7 +3632,13 @@ function Sidebar({
3320
3632
  isSubmitting: isAuthSubmitting
3321
3633
  }) : /* @__PURE__ */ jsx18(AuthenticatedContent, {
3322
3634
  isConnected,
3635
+ isPaused,
3636
+ isPausing,
3637
+ isDisconnecting,
3638
+ canResume: resumableSession !== null,
3323
3639
  onStartSession: startSession,
3640
+ onPause: pauseSession,
3641
+ onResume: resumeSession,
3324
3642
  onDisconnect: disconnect,
3325
3643
  isStarting,
3326
3644
  error,
@@ -3339,7 +3657,13 @@ function Sidebar({
3339
3657
  }
3340
3658
  function AuthenticatedContent({
3341
3659
  isConnected,
3660
+ isPaused,
3661
+ isPausing,
3662
+ isDisconnecting,
3663
+ canResume,
3342
3664
  onStartSession,
3665
+ onPause,
3666
+ onResume,
3343
3667
  onDisconnect,
3344
3668
  isStarting,
3345
3669
  error,
@@ -3353,9 +3677,18 @@ function AuthenticatedContent({
3353
3677
  agentId,
3354
3678
  agentControls
3355
3679
  }) {
3356
- const showSelectorAsPrompt = hasModuleSelector && !isConnected && !isStarting;
3680
+ const inSession = isConnected || isStarting || isPaused || isPausing;
3681
+ const showSelectorAsPrompt = hasModuleSelector && !inSession && !isDisconnecting;
3357
3682
  const showTabBar = !showSelectorAsPrompt;
3358
- return /* @__PURE__ */ jsxs16(Fragment3, {
3683
+ let transitionLabel = null;
3684
+ if (isPausing) {
3685
+ transitionLabel = "Pausing...";
3686
+ } else if (isStarting && !isConnected) {
3687
+ transitionLabel = "Reconnecting...";
3688
+ } else if (isPaused) {
3689
+ transitionLabel = "Paused";
3690
+ }
3691
+ return /* @__PURE__ */ jsxs16(Fragment4, {
3359
3692
  children: [
3360
3693
  isConnected && /* @__PURE__ */ jsx18(ConnectedBanner, {}),
3361
3694
  showTabBar && /* @__PURE__ */ jsxs16("div", {
@@ -3393,9 +3726,22 @@ function AuthenticatedContent({
3393
3726
  }),
3394
3727
  /* @__PURE__ */ jsx18("div", {
3395
3728
  className: "skippr:flex skippr:min-h-0 skippr:flex-1 skippr:flex-col",
3396
- children: showSelectorAsPrompt ? /* @__PURE__ */ jsx18(ModuleSelector, {}) : isConnected || isStarting ? /* @__PURE__ */ jsx18(ConnectedBody, {
3397
- activeTab,
3398
- autoFocusChat
3729
+ children: isDisconnecting ? /* @__PURE__ */ jsx18("div", {
3730
+ className: "skippr:flex skippr:flex-1 skippr:items-center skippr:justify-center",
3731
+ children: /* @__PURE__ */ jsx18(LoadingDots, {
3732
+ label: "Ending session..."
3733
+ })
3734
+ }) : showSelectorAsPrompt ? /* @__PURE__ */ jsx18(ModuleSelector, {}) : inSession ? /* @__PURE__ */ jsxs16(Fragment4, {
3735
+ children: [
3736
+ transitionLabel && /* @__PURE__ */ jsx18("div", {
3737
+ className: "skippr:shrink-0 skippr:border-b skippr:border-border skippr:bg-muted/50 skippr:px-3 skippr:py-1.5 skippr:text-center skippr:text-xs skippr:text-muted-foreground",
3738
+ children: transitionLabel
3739
+ }),
3740
+ /* @__PURE__ */ jsx18(ConnectedBody, {
3741
+ activeTab,
3742
+ autoFocusChat
3743
+ })
3744
+ ]
3399
3745
  }) : /* @__PURE__ */ jsx18("div", {
3400
3746
  className: "skippr:flex skippr:min-h-0 skippr:flex-1 skippr:flex-col skippr:animate-skippr-tab-fade",
3401
3747
  children: /* @__PURE__ */ jsx18(StartSessionPrompt, {
@@ -3404,12 +3750,18 @@ function AuthenticatedContent({
3404
3750
  agentControls,
3405
3751
  isStarting,
3406
3752
  error,
3407
- label: startSessionLabel
3753
+ label: startSessionLabel,
3754
+ canResume,
3755
+ onResume
3408
3756
  })
3409
3757
  }, `${activeTab}-empty`)
3410
3758
  }),
3411
- isConnected && !hideControls && /* @__PURE__ */ jsx18(MeetingControls, {
3759
+ (isConnected || isPaused) && !isDisconnecting && !hideControls && /* @__PURE__ */ jsx18(MeetingControls, {
3412
3760
  onHangUp: onDisconnect,
3761
+ onPause,
3762
+ onResume,
3763
+ isPaused,
3764
+ isPausing,
3413
3765
  showScreenShareToggle
3414
3766
  })
3415
3767
  ]
@@ -3527,10 +3879,16 @@ function LiveAgent(props) {
3527
3879
  shouldConnect,
3528
3880
  isStarting,
3529
3881
  isDisconnecting,
3882
+ isPausing,
3530
3883
  error,
3531
3884
  errorCode,
3532
3885
  startSession,
3886
+ pauseSession,
3887
+ resumeSession: resumeSessionById,
3533
3888
  disconnect,
3889
+ isPaused,
3890
+ resumableSessions,
3891
+ historyMessages,
3534
3892
  pendingScreenStream,
3535
3893
  bearerToken
3536
3894
  } = useSession({
@@ -3542,9 +3900,23 @@ function LiveAgent(props) {
3542
3900
  onStartError: expandOnSessionStartError,
3543
3901
  onDisconnect: minimizeOnSessionDisconnect
3544
3902
  });
3903
+ const teardownInFlightRef = useRef10(false);
3904
+ teardownInFlightRef.current = isPaused || isPausing || isDisconnecting;
3905
+ const handleRoomDisconnected = useCallback8(() => {
3906
+ if (teardownInFlightRef.current)
3907
+ return;
3908
+ disconnect();
3909
+ }, [disconnect]);
3910
+ const resumableSession = resumableSessions.find((s) => s.agentId === agentId) ?? null;
3911
+ const resumeSession = useCallback8(async () => {
3912
+ if (!resumableSession)
3913
+ return;
3914
+ await resumeSessionById({ sessionId: resumableSession.id, agentControls });
3915
+ }, [resumableSession, resumeSessionById, agentControls]);
3545
3916
  const [isPanelOpen, setIsPanelOpen] = useState11(defaultOpen);
3546
3917
  const [isMinimized, setIsMinimized] = useState11(minimizable && !defaultOpen);
3547
3918
  const [sidebarTab, setSidebarTab] = useState11("agenda");
3919
+ const [phasesSnapshot, setPhasesSnapshot] = useState11([]);
3548
3920
  const {
3549
3921
  modules: availableModules,
3550
3922
  isLoading: isLoadingModules,
@@ -3559,8 +3931,13 @@ function LiveAgent(props) {
3559
3931
  if (!found)
3560
3932
  return;
3561
3933
  setActiveModule(found);
3562
- startSession({ agentId: found.id, agentControls: found.controls });
3563
- }, [availableModules, startSession]);
3934
+ const resumable = resumableSessions.find((s) => s.agentId === found.id);
3935
+ if (resumable) {
3936
+ resumeSessionById({ sessionId: resumable.id, agentControls: found.controls });
3937
+ } else {
3938
+ startSession({ agentId: found.id, agentControls: found.controls });
3939
+ }
3940
+ }, [availableModules, resumableSessions, startSession, resumeSessionById]);
3564
3941
  const [welcomeDismissed, setWelcomeDismissed] = useState11(false);
3565
3942
  const dismissWelcome = useCallback8(() => setWelcomeDismissed(true), []);
3566
3943
  const [currentPosition, setCurrentPosition] = useState11(() => {
@@ -3592,16 +3969,25 @@ function LiveAgent(props) {
3592
3969
  }, [minimizable]);
3593
3970
  const isConnected = connection !== null;
3594
3971
  const isAuthenticated = !!userToken || !!authTokenProp || auth.isAuthenticated;
3595
- const ctx = useMemo5(() => ({
3972
+ const ctx = useMemo6(() => ({
3596
3973
  connection,
3597
3974
  shouldConnect,
3598
3975
  isConnected,
3599
3976
  isStarting,
3600
3977
  isDisconnecting,
3978
+ isPausing,
3601
3979
  error,
3602
3980
  errorCode,
3603
3981
  startSession,
3982
+ pauseSession,
3983
+ resumeSession,
3604
3984
  disconnect,
3985
+ isPaused,
3986
+ resumableSession,
3987
+ resumableSessions,
3988
+ historyMessages,
3989
+ phasesSnapshot,
3990
+ setPhasesSnapshot,
3605
3991
  isPanelOpen,
3606
3992
  openPanel,
3607
3993
  closePanel,
@@ -3639,10 +4025,18 @@ function LiveAgent(props) {
3639
4025
  isConnected,
3640
4026
  isStarting,
3641
4027
  isDisconnecting,
4028
+ isPausing,
3642
4029
  error,
3643
4030
  errorCode,
3644
4031
  startSession,
4032
+ pauseSession,
4033
+ resumeSession,
3645
4034
  disconnect,
4035
+ isPaused,
4036
+ resumableSession,
4037
+ resumableSessions,
4038
+ historyMessages,
4039
+ phasesSnapshot,
3646
4040
  isPanelOpen,
3647
4041
  openPanel,
3648
4042
  closePanel,
@@ -3681,7 +4075,7 @@ function LiveAgent(props) {
3681
4075
  token: connection?.token,
3682
4076
  connect: shouldConnect,
3683
4077
  audio: true,
3684
- onDisconnected: disconnect,
4078
+ onDisconnected: handleRoomDisconnected,
3685
4079
  children: [
3686
4080
  connection && /* @__PURE__ */ jsx20(RoomAudioRenderer, {}),
3687
4081
  connection && captureMode === "screenshare" && /* @__PURE__ */ jsx20(AutoStartMedia, {