@skippr/live-agent-sdk 0.34.0 → 0.36.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 useMemo5, 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,9 @@ 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 [historyMessages, setHistoryMessages] = useState3([]);
241
277
  useEffect3(() => {
242
278
  let stale = false;
243
279
  if (authToken) {
@@ -257,12 +293,29 @@ function useSession({
257
293
  stale = true;
258
294
  };
259
295
  }, [authToken, appKey, userToken]);
260
- const startSession = useCallback3(async ({ agentId, agentControls }) => {
296
+ const pauseOnUnloadRef = useRef(null);
297
+ pauseOnUnloadRef.current = connection !== null && !isPaused && sessionId && bearerToken ? { sessionId, bearerToken } : null;
298
+ useEffect3(() => {
299
+ const onPageHide = () => {
300
+ const pending = pauseOnUnloadRef.current;
301
+ if (!pending)
302
+ return;
303
+ fetch(`${API_URL3}/v1/sessions/${pending.sessionId}/pause`, {
304
+ method: "POST",
305
+ credentials: "omit",
306
+ keepalive: true,
307
+ headers: { Authorization: `Bearer ${pending.bearerToken}` }
308
+ }).catch(() => {});
309
+ };
310
+ window.addEventListener("pagehide", onPageHide);
311
+ return () => window.removeEventListener("pagehide", onPageHide);
312
+ }, []);
313
+ const startSession = useCallback3(async ({ agentId, agentControls, existingSessionId }) => {
261
314
  if (!bearerToken) {
262
315
  setError("No auth token available");
263
316
  return;
264
317
  }
265
- if (!agentId) {
318
+ if (!agentId && !existingSessionId) {
266
319
  setError("No agent selected");
267
320
  return;
268
321
  }
@@ -272,84 +325,109 @@ function useSession({
272
325
  onStart?.();
273
326
  let screenStream = null;
274
327
  if (captureMode === "screenshare") {
275
- try {
276
- screenStream = await navigator.mediaDevices.getDisplayMedia({
277
- video: { displaySurface: "browser" }
278
- });
279
- } catch {
280
- screenStream = null;
281
- }
328
+ screenStream = await requestScreenShare();
282
329
  }
283
330
  const requestAgentControls = agentControls?.highlight === true ? { highlight: true } : undefined;
284
331
  const headers = { Authorization: `Bearer ${bearerToken}` };
285
332
  try {
286
- const createResp = await fetch(`${API_URL3}/v1/sessions`, {
287
- method: "POST",
288
- credentials: "omit",
289
- headers: { "Content-Type": "application/json", ...headers },
290
- body: JSON.stringify({
291
- agentId,
292
- captureMode,
293
- agentControls: requestAgentControls
294
- })
295
- });
296
- if (!createResp.ok) {
297
- const body = await createResp.json().catch(() => null);
298
- setErrorCode(createResp.status);
299
- throw new Error(body?.detail || `Failed to create session: ${createResp.status}`);
333
+ let resolvedSessionId = existingSessionId;
334
+ if (!resolvedSessionId) {
335
+ const createResp = await fetch(`${API_URL3}/v1/sessions`, {
336
+ method: "POST",
337
+ credentials: "omit",
338
+ headers: { "Content-Type": "application/json", ...headers },
339
+ body: JSON.stringify({
340
+ agentId,
341
+ captureMode,
342
+ agentControls: requestAgentControls
343
+ })
344
+ });
345
+ if (!createResp.ok) {
346
+ const body = await createResp.json().catch(() => null);
347
+ setErrorCode(createResp.status);
348
+ throw new Error(body?.detail || `Failed to create session: ${createResp.status}`);
349
+ }
350
+ const { session: session2 } = await createResp.json();
351
+ resolvedSessionId = session2.id;
300
352
  }
301
- const { session } = await createResp.json();
302
- const startResp = await fetch(`${API_URL3}/v1/sessions/${session.id}/start`, {
353
+ const startResp = await fetch(`${API_URL3}/v1/sessions/${resolvedSessionId}/start`, {
303
354
  method: "POST",
304
355
  credentials: "omit",
305
- headers
356
+ headers: { "Content-Type": "application/json", ...headers },
357
+ body: JSON.stringify({ captureMode, agentControls: requestAgentControls })
306
358
  });
307
359
  if (!startResp.ok) {
308
360
  const body = await startResp.json().catch(() => null);
309
361
  setErrorCode(startResp.status);
310
362
  throw new Error(body?.detail || `Failed to start session: ${startResp.status}`);
311
363
  }
312
- const { connection: conn } = await startResp.json();
364
+ const { session, connection: conn } = await startResp.json();
365
+ const history2 = existingSessionId ? await fetchSessionMessages(session.id, bearerToken) : [];
313
366
  setSessionId(session.id);
367
+ setHistoryMessages(history2);
314
368
  setConnection({
315
369
  livekitUrl: conn.livekitUrl,
316
370
  token: conn.token
317
371
  });
318
372
  setPendingScreenStream(screenStream);
319
373
  setShouldConnect(true);
374
+ setIsPaused(false);
320
375
  } catch (e) {
321
- if (screenStream) {
322
- for (const track of screenStream.getTracks())
323
- track.stop();
324
- }
376
+ stopStream(screenStream);
325
377
  setError(e instanceof Error ? e.message : "Failed to start session");
326
378
  onStartError?.();
327
379
  } finally {
328
380
  setIsStarting(false);
329
381
  }
330
382
  }, [captureMode, bearerToken, onStart, onStartError]);
383
+ const pauseSession = useCallback3(async () => {
384
+ if (!sessionId || !bearerToken)
385
+ return;
386
+ setIsPausing(true);
387
+ try {
388
+ const resp = await fetch(`${API_URL3}/v1/sessions/${sessionId}/pause`, {
389
+ method: "POST",
390
+ credentials: "omit",
391
+ headers: { Authorization: `Bearer ${bearerToken}` }
392
+ });
393
+ if (!resp.ok) {
394
+ const body = await resp.json().catch(() => null);
395
+ throw new Error(body?.detail || `Failed to pause: ${resp.status}`);
396
+ }
397
+ } catch (e) {
398
+ setError(e instanceof Error ? e.message : "Failed to pause session");
399
+ setIsPausing(false);
400
+ return;
401
+ }
402
+ const history2 = await fetchSessionMessages(sessionId, bearerToken);
403
+ stopStream(pendingScreenStream);
404
+ setPendingScreenStream(null);
405
+ setShouldConnect(false);
406
+ setConnection(null);
407
+ setHistoryMessages(history2);
408
+ setIsPaused(true);
409
+ setIsPausing(false);
410
+ }, [sessionId, bearerToken, pendingScreenStream]);
331
411
  const disconnect = useCallback3(async () => {
332
412
  setIsDisconnecting(true);
333
413
  try {
334
414
  if (sessionId && bearerToken) {
335
415
  try {
336
- await fetch(`${API_URL3}/v1/sessions/${sessionId}/complete`, {
416
+ await fetch(`${API_URL3}/v1/sessions/${sessionId}/pause`, {
337
417
  method: "POST",
338
418
  credentials: "omit",
339
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${bearerToken}` },
340
- body: JSON.stringify({})
419
+ headers: { Authorization: `Bearer ${bearerToken}` }
341
420
  });
342
421
  } catch {}
343
422
  }
344
- if (pendingScreenStream) {
345
- for (const track of pendingScreenStream.getTracks())
346
- track.stop();
347
- }
423
+ stopStream(pendingScreenStream);
348
424
  setError("");
349
425
  setShouldConnect(false);
350
426
  setConnection(null);
351
427
  setSessionId(null);
352
428
  setPendingScreenStream(null);
429
+ setHistoryMessages([]);
430
+ setIsPaused(false);
353
431
  onDisconnect?.();
354
432
  } finally {
355
433
  setIsDisconnecting(false);
@@ -363,7 +441,11 @@ function useSession({
363
441
  error,
364
442
  errorCode,
365
443
  startSession,
444
+ pauseSession,
366
445
  disconnect,
446
+ isPaused,
447
+ isPausing,
448
+ historyMessages,
367
449
  pendingScreenStream,
368
450
  bearerToken
369
451
  };
@@ -379,14 +461,35 @@ var DOM_EVENTS_TOPIC = "skippr.dom-events";
379
461
  var HIGHLIGHT_TOPIC = "skippr.highlight";
380
462
  var NAME_MAX_CHARS = 80;
381
463
 
464
+ // src/lib/session.ts
465
+ function getResumableSessionId(currentSession, mode) {
466
+ if (!currentSession)
467
+ return;
468
+ const { id, status } = currentSession;
469
+ if (status === "paused" || status === "active")
470
+ return id;
471
+ if (status === "expired" && mode === "always_on")
472
+ return id;
473
+ return;
474
+ }
475
+ function canResumeSession(currentSession, mode) {
476
+ return getResumableSessionId(currentSession, mode) !== undefined;
477
+ }
478
+ function getResumableSession(module) {
479
+ if (!module)
480
+ return null;
481
+ const id = getResumableSessionId(module.currentSession, module.mode);
482
+ return id ? { id, agentId: module.id } : null;
483
+ }
484
+
382
485
  // src/components/AutoStartMedia.tsx
383
486
  import { useConnectionState, useLocalParticipant } from "@livekit/components-react/hooks";
384
487
  import { ConnectionState, Track } from "livekit-client";
385
- import { useEffect as useEffect4, useRef } from "react";
488
+ import { useEffect as useEffect4, useRef as useRef2 } from "react";
386
489
  function AutoStartMedia({ pendingScreenStream }) {
387
490
  const { localParticipant } = useLocalParticipant();
388
491
  const connectionState = useConnectionState();
389
- const didStartRef = useRef(false);
492
+ const didStartRef = useRef2(false);
390
493
  useEffect4(() => {
391
494
  if (didStartRef.current)
392
495
  return;
@@ -412,7 +515,7 @@ function AutoStartMedia({ pendingScreenStream }) {
412
515
  // src/components/DomCapture.tsx
413
516
  import { useConnectionState as useConnectionState2, useLocalParticipant as useLocalParticipant2 } from "@livekit/components-react/hooks";
414
517
  import { ConnectionState as ConnectionState2, ScreenSharePresets, Track as Track2 } from "livekit-client";
415
- import { useEffect as useEffect5, useRef as useRef2 } from "react";
518
+ import { useEffect as useEffect5, useRef as useRef3 } from "react";
416
519
 
417
520
  // src/capture/a11yUtils.ts
418
521
  var ROLE_BY_TAG = {
@@ -1159,7 +1262,7 @@ async function unpublishAndStopTrack(localParticipant, videoTrack) {
1159
1262
  function DomCapture() {
1160
1263
  const { localParticipant } = useLocalParticipant2();
1161
1264
  const connectionState = useConnectionState2();
1162
- const didStartRef = useRef2(false);
1265
+ const didStartRef = useRef3(false);
1163
1266
  useEffect5(() => {
1164
1267
  if (didStartRef.current)
1165
1268
  return;
@@ -1499,8 +1602,14 @@ var __iconNode18 = [
1499
1602
  ]
1500
1603
  ];
1501
1604
  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
1605
+ // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/pause.js
1503
1606
  var __iconNode19 = [
1607
+ ["rect", { x: "14", y: "3", width: "5", height: "18", rx: "1", key: "kaeet6" }],
1608
+ ["rect", { x: "5", y: "3", width: "5", height: "18", rx: "1", key: "1wsw3u" }]
1609
+ ];
1610
+ var Pause = createLucideIcon("pause", __iconNode19);
1611
+ // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/phone-off.js
1612
+ var __iconNode20 = [
1504
1613
  [
1505
1614
  "path",
1506
1615
  {
@@ -1517,9 +1626,20 @@ var __iconNode19 = [
1517
1626
  }
1518
1627
  ]
1519
1628
  ];
1520
- var PhoneOff = createLucideIcon("phone-off", __iconNode19);
1629
+ var PhoneOff = createLucideIcon("phone-off", __iconNode20);
1630
+ // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/play.js
1631
+ var __iconNode21 = [
1632
+ [
1633
+ "path",
1634
+ {
1635
+ 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",
1636
+ key: "10ikf1"
1637
+ }
1638
+ ]
1639
+ ];
1640
+ var Play = createLucideIcon("play", __iconNode21);
1521
1641
  // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/rocket.js
1522
- var __iconNode20 = [
1642
+ var __iconNode22 = [
1523
1643
  ["path", { d: "M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5", key: "qeys4" }],
1524
1644
  [
1525
1645
  "path",
@@ -1537,9 +1657,9 @@ var __iconNode20 = [
1537
1657
  ],
1538
1658
  ["path", { d: "M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05", key: "92ym6u" }]
1539
1659
  ];
1540
- var Rocket = createLucideIcon("rocket", __iconNode20);
1660
+ var Rocket = createLucideIcon("rocket", __iconNode22);
1541
1661
  // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/send.js
1542
- var __iconNode21 = [
1662
+ var __iconNode23 = [
1543
1663
  [
1544
1664
  "path",
1545
1665
  {
@@ -1549,17 +1669,17 @@ var __iconNode21 = [
1549
1669
  ],
1550
1670
  ["path", { d: "m21.854 2.147-10.94 10.939", key: "12cjpa" }]
1551
1671
  ];
1552
- var Send = createLucideIcon("send", __iconNode21);
1672
+ var Send = createLucideIcon("send", __iconNode23);
1553
1673
  // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/icons/user-plus.js
1554
- var __iconNode22 = [
1674
+ var __iconNode24 = [
1555
1675
  ["path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2", key: "1yyitq" }],
1556
1676
  ["circle", { cx: "9", cy: "7", r: "4", key: "nufk8" }],
1557
1677
  ["line", { x1: "19", x2: "19", y1: "8", y2: "14", key: "1bvyxn" }],
1558
1678
  ["line", { x1: "22", x2: "16", y1: "11", y2: "11", key: "1shjgl" }]
1559
1679
  ];
1560
- var UserPlus = createLucideIcon("user-plus", __iconNode22);
1680
+ var UserPlus = createLucideIcon("user-plus", __iconNode24);
1561
1681
  // src/components/HighlightOverlay.tsx
1562
- import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef3, useState as useState4 } from "react";
1682
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef4, useState as useState4 } from "react";
1563
1683
  import { jsx, jsxs } from "react/jsx-runtime";
1564
1684
  var Z_INDEX = 2147483646;
1565
1685
  var HIGHLIGHT_PADDING = 6;
@@ -1664,8 +1784,8 @@ function findScrollableAncestor(el) {
1664
1784
  }
1665
1785
  function HighlightOverlay() {
1666
1786
  const [overlayState, setOverlayState] = useState4(null);
1667
- const targetElementRef = useRef3(null);
1668
- const pendingFrameRef = useRef3(null);
1787
+ const targetElementRef = useRef4(null);
1788
+ const pendingFrameRef = useRef4(null);
1669
1789
  const clearOverlay = useCallback4(() => {
1670
1790
  targetElementRef.current = null;
1671
1791
  setOverlayState(null);
@@ -1792,14 +1912,27 @@ function HighlightOverlay() {
1792
1912
  // src/components/MinimizedBubble.tsx
1793
1913
  import { useEffect as useEffect7 } from "react";
1794
1914
 
1915
+ // src/lib/react-compat.ts
1916
+ import * as React from "react";
1917
+ function selectContextHook(use2, useContext3) {
1918
+ return use2 ?? useContext3;
1919
+ }
1920
+ var useContextValue = selectContextHook(React.use, React.useContext);
1921
+
1795
1922
  // src/hooks/useLiveAgent.ts
1796
- import { use } from "react";
1797
1923
  function useLiveAgent() {
1798
- const ctx = use(LiveAgentContext);
1924
+ const ctx = useContextValue(LiveAgentContext);
1799
1925
  if (!ctx) {
1800
1926
  throw new Error("useLiveAgent must be used within a <LiveAgent> provider");
1801
1927
  }
1802
- const { connection, shouldConnect, ...publicValue } = ctx;
1928
+ const {
1929
+ connection,
1930
+ shouldConnect,
1931
+ historyMessages,
1932
+ phasesSnapshot,
1933
+ setPhasesSnapshot,
1934
+ ...publicValue
1935
+ } = ctx;
1803
1936
  return publicValue;
1804
1937
  }
1805
1938
 
@@ -1861,14 +1994,29 @@ function pillStatusFromAgent(state, canSeePage) {
1861
1994
  return canSeePage ? "observing" : "connected";
1862
1995
  }
1863
1996
  function LauncherStatusPill() {
1864
- const { isConnected, isStarting, expandPanel, setSidebarTab, position, captureMode } = useLiveAgent();
1997
+ const { isConnected, isStarting, isPaused, expandPanel, setSidebarTab, position, captureMode } = useLiveAgent();
1865
1998
  const { state } = useAgentVoiceState();
1866
1999
  const { isScreenSharing } = useMediaControls();
1867
2000
  const isStandingBy = isStarting && !isConnected;
1868
- if (!isConnected && !isStandingBy)
2001
+ if (!isConnected && !isStandingBy && !isPaused)
1869
2002
  return null;
1870
2003
  const canSeePage = captureMode === "auto" || isScreenSharing;
1871
- const status = isStandingBy ? "standing-by" : pillStatusFromAgent(state, canSeePage);
2004
+ let status;
2005
+ if (isPaused) {
2006
+ status = "paused";
2007
+ } else if (isStandingBy) {
2008
+ status = "standing-by";
2009
+ } else {
2010
+ status = pillStatusFromAgent(state, canSeePage);
2011
+ }
2012
+ let ariaLabel;
2013
+ if (isStandingBy) {
2014
+ ariaLabel = "Skippr is standing by — click to open";
2015
+ } else if (isPaused) {
2016
+ ariaLabel = "Session paused — click to open";
2017
+ } else {
2018
+ ariaLabel = `Skippr is ${status} — click to open chat`;
2019
+ }
1872
2020
  const handleClick = () => {
1873
2021
  if (!isStandingBy)
1874
2022
  setSidebarTab("chat");
@@ -1878,7 +2026,7 @@ function LauncherStatusPill() {
1878
2026
  type: "button",
1879
2027
  onClick: handleClick,
1880
2028
  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`,
2029
+ "aria-label": ariaLabel,
1882
2030
  children: /* @__PURE__ */ jsxs2("span", {
1883
2031
  className: "skippr:flex skippr:items-center skippr:gap-2 skippr:animate-[skippr-pill-content_0.22s_ease-out]",
1884
2032
  children: [
@@ -1921,6 +2069,14 @@ function LauncherStatusPill() {
1921
2069
  children: "Skippr is thinking"
1922
2070
  })
1923
2071
  ]
2072
+ }),
2073
+ status === "paused" && /* @__PURE__ */ jsxs2(Fragment, {
2074
+ children: [
2075
+ /* @__PURE__ */ jsx2(PausedDot, {}),
2076
+ /* @__PURE__ */ jsx2("span", {
2077
+ children: "Session paused"
2078
+ })
2079
+ ]
1924
2080
  })
1925
2081
  ]
1926
2082
  }, status)
@@ -1939,6 +2095,11 @@ function ConnectedDot() {
1939
2095
  ]
1940
2096
  });
1941
2097
  }
2098
+ function PausedDot() {
2099
+ return /* @__PURE__ */ jsx2("span", {
2100
+ className: "skippr:inline-flex skippr:size-2 skippr:rounded-full skippr:bg-amber-400"
2101
+ });
2102
+ }
1942
2103
  function ObservingIcon() {
1943
2104
  return /* @__PURE__ */ jsxs2("svg", {
1944
2105
  viewBox: "0 0 24 24",
@@ -2090,34 +2251,60 @@ import { jsx as jsx4, jsxs as jsxs4, Fragment as Fragment2 } from "react/jsx-run
2090
2251
  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
2252
  var CONTROL_SHADOW = "skippr:shadow-[0_4px_16px_rgba(0,0,0,0.15),0_2px_4px_rgba(0,0,0,0.1)]";
2092
2253
  function ConnectedLauncher() {
2093
- const { expandPanel, disconnect, captureMode, setSidebarTab } = useLiveAgent();
2254
+ const {
2255
+ expandPanel,
2256
+ disconnect,
2257
+ pauseSession,
2258
+ resumeSession,
2259
+ isPaused,
2260
+ isPausing,
2261
+ captureMode,
2262
+ setSidebarTab
2263
+ } = useLiveAgent();
2094
2264
  const { isMuted, toggleMute, isScreenSharing, toggleScreenShare } = useMediaControls();
2095
2265
  const showScreenShareToggle = captureMode === "screenshare";
2266
+ const showPaused = isPaused || isPausing;
2096
2267
  const openChat = () => {
2097
2268
  setSidebarTab("chat");
2098
2269
  expandPanel();
2099
2270
  };
2100
2271
  return /* @__PURE__ */ jsxs4(Fragment2, {
2101
2272
  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
- })
2273
+ !showPaused && /* @__PURE__ */ jsxs4(Fragment2, {
2274
+ children: [
2275
+ /* @__PURE__ */ jsx4("button", {
2276
+ type: "button",
2277
+ onClick: toggleMute,
2278
+ "aria-label": isMuted ? "Unmute" : "Mute",
2279
+ 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"),
2280
+ children: isMuted ? /* @__PURE__ */ jsx4(MicOff, {
2281
+ className: "skippr:size-5"
2282
+ }) : /* @__PURE__ */ jsx4(Mic, {
2283
+ className: "skippr:size-5"
2284
+ })
2285
+ }),
2286
+ showScreenShareToggle && /* @__PURE__ */ jsx4("button", {
2287
+ type: "button",
2288
+ onClick: toggleScreenShare,
2289
+ "aria-label": isScreenSharing ? "Stop sharing screen" : "Share screen",
2290
+ 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"),
2291
+ children: isScreenSharing ? /* @__PURE__ */ jsx4(MonitorOff, {
2292
+ className: "skippr:size-5"
2293
+ }) : /* @__PURE__ */ jsx4(Monitor, {
2294
+ className: "skippr:size-5"
2295
+ })
2296
+ })
2297
+ ]
2112
2298
  }),
2113
- showScreenShareToggle && /* @__PURE__ */ jsx4("button", {
2299
+ /* @__PURE__ */ jsx4("button", {
2114
2300
  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, {
2301
+ onClick: () => isPaused ? resumeSession() : pauseSession(),
2302
+ disabled: isPausing,
2303
+ "aria-label": showPaused ? "Resume session" : "Pause session",
2304
+ className: cn(CONTROL_BUTTON, CONTROL_SHADOW, "skippr:bg-white skippr:text-foreground skippr:hover:bg-muted skippr:disabled:opacity-60"),
2305
+ children: showPaused ? /* @__PURE__ */ jsx4(Play, {
2119
2306
  className: "skippr:size-5"
2120
- }) : /* @__PURE__ */ jsx4(Monitor, {
2307
+ }) : /* @__PURE__ */ jsx4(Pause, {
2121
2308
  className: "skippr:size-5"
2122
2309
  })
2123
2310
  }),
@@ -2130,7 +2317,7 @@ function ConnectedLauncher() {
2130
2317
  className: "skippr:size-5"
2131
2318
  })
2132
2319
  }),
2133
- /* @__PURE__ */ jsx4("button", {
2320
+ !showPaused && /* @__PURE__ */ jsx4("button", {
2134
2321
  type: "button",
2135
2322
  onClick: openChat,
2136
2323
  "aria-label": "Open chat",
@@ -2181,8 +2368,8 @@ function MinimizedBubble({
2181
2368
  welcomeDismissed,
2182
2369
  onDismissWelcome
2183
2370
  }) {
2184
- const { isConnected, isStarting, position } = useLiveAgent();
2185
- const inSession = isConnected;
2371
+ const { isConnected, isStarting, isPaused, isPausing, position } = useLiveAgent();
2372
+ const inSession = isConnected || isPaused || isPausing;
2186
2373
  return /* @__PURE__ */ jsxs4(Fragment2, {
2187
2374
  children: [
2188
2375
  /* @__PURE__ */ jsx4(LauncherStatusPill, {}),
@@ -2202,7 +2389,7 @@ function MinimizedBubble({
2202
2389
  }
2203
2390
 
2204
2391
  // src/components/Sidebar.tsx
2205
- import { useEffect as useEffect15 } from "react";
2392
+ import { useEffect as useEffect16 } from "react";
2206
2393
 
2207
2394
  // src/hooks/useCombinedMessages.ts
2208
2395
  import { useMemo as useMemo4 } from "react";
@@ -2273,18 +2460,25 @@ function useCombinedMessages() {
2273
2460
  const { transcriptMessages } = useStreamingTranscript();
2274
2461
  const { chatMessages, sendChatMessage, isSendingChat } = useChatMessages();
2275
2462
  const { state: agentState } = useAgentVoiceState();
2276
- const allMessages = useMemo4(() => {
2463
+ const historyMessages = useContextValue(LiveAgentContext)?.historyMessages ?? [];
2464
+ const liveMessages = useMemo4(() => {
2277
2465
  if (chatMessages.length === 0)
2278
2466
  return transcriptMessages;
2279
2467
  if (transcriptMessages.length === 0)
2280
2468
  return chatMessages;
2281
2469
  return mergeChatsIntoTranscripts(transcriptMessages, chatMessages);
2282
2470
  }, [transcriptMessages, chatMessages]);
2471
+ const allMessages = useMemo4(() => {
2472
+ if (historyMessages.length === 0)
2473
+ return liveMessages;
2474
+ const seenIds = new Set(liveMessages.map((message) => message.id));
2475
+ return [...historyMessages.filter((message) => !seenIds.has(message.id)), ...liveMessages];
2476
+ }, [historyMessages, liveMessages]);
2283
2477
  return { allMessages, agentState, sendChatMessage, isSendingChat };
2284
2478
  }
2285
2479
 
2286
2480
  // src/hooks/usePhaseUpdates.ts
2287
- import { useCallback as useCallback6 } from "react";
2481
+ import { useCallback as useCallback6, useEffect as useEffect9 } from "react";
2288
2482
 
2289
2483
  // src/hooks/useAgentState.ts
2290
2484
  import { useRemoteParticipants } from "@livekit/components-react/hooks";
@@ -2339,12 +2533,18 @@ function parsePhases(json) {
2339
2533
  }
2340
2534
  function usePhaseUpdates() {
2341
2535
  const parse = useCallback6(parsePhases, []);
2342
- const phases = useAgentState("phases", parse, []);
2536
+ const livePhases = useAgentState("phases", parse, []);
2537
+ const ctx = useContextValue(LiveAgentContext);
2538
+ useEffect9(() => {
2539
+ if (livePhases.length > 0)
2540
+ ctx?.setPhasesSnapshot(livePhases);
2541
+ }, [livePhases, ctx?.setPhasesSnapshot]);
2542
+ const phases = livePhases.length > 0 ? livePhases : ctx?.phasesSnapshot ?? [];
2343
2543
  return { phases };
2344
2544
  }
2345
2545
 
2346
2546
  // src/hooks/useSessionRemaining.ts
2347
- import { useEffect as useEffect9, useRef as useRef4, useState as useState6 } from "react";
2547
+ import { useEffect as useEffect10, useRef as useRef5, useState as useState6 } from "react";
2348
2548
 
2349
2549
  // src/lib/format.ts
2350
2550
  function formatTime(seconds) {
@@ -2360,9 +2560,9 @@ function parseNumber(s) {
2360
2560
  // src/hooks/useSessionRemaining.ts
2361
2561
  function useSessionRemaining() {
2362
2562
  const maxCallDuration = useAgentState("maxCallDuration", parseNumber, null);
2363
- const endTimeRef = useRef4(null);
2563
+ const endTimeRef = useRef5(null);
2364
2564
  const [remaining, setRemaining] = useState6(null);
2365
- useEffect9(() => {
2565
+ useEffect10(() => {
2366
2566
  if (maxCallDuration === null || endTimeRef.current !== null)
2367
2567
  return;
2368
2568
  const endTime = Date.now() + maxCallDuration * 1000;
@@ -2379,10 +2579,10 @@ function useSessionRemaining() {
2379
2579
  }
2380
2580
 
2381
2581
  // src/hooks/useElapsedSeconds.ts
2382
- import { useEffect as useEffect10, useState as useState7 } from "react";
2582
+ import { useEffect as useEffect11, useState as useState7 } from "react";
2383
2583
  function useElapsedSeconds(isRunning) {
2384
2584
  const [elapsed, setElapsed] = useState7(0);
2385
- useEffect10(() => {
2585
+ useEffect11(() => {
2386
2586
  if (!isRunning) {
2387
2587
  setElapsed(0);
2388
2588
  return;
@@ -2480,7 +2680,7 @@ function LoadingDots({ label }) {
2480
2680
  }
2481
2681
 
2482
2682
  // src/components/LoginFlow.tsx
2483
- import { useCallback as useCallback7, useEffect as useEffect11, useRef as useRef5, useState as useState8 } from "react";
2683
+ import { useCallback as useCallback7, useEffect as useEffect12, useRef as useRef6, useState as useState8 } from "react";
2484
2684
 
2485
2685
  // src/components/ui/button.tsx
2486
2686
  import { forwardRef as forwardRef3 } from "react";
@@ -2608,16 +2808,16 @@ function EmailStep({ email, onEmailChange, onSubmit, error, isSubmitting }) {
2608
2808
  function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
2609
2809
  const [digits, setDigits] = useState8(Array(OTP_LENGTH).fill(""));
2610
2810
  const [resendCooldown, setResendCooldown] = useState8(0);
2611
- const inputRefs = useRef5([]);
2612
- const submittedRef = useRef5(false);
2613
- useEffect11(() => {
2811
+ const inputRefs = useRef6([]);
2812
+ const submittedRef = useRef6(false);
2813
+ useEffect12(() => {
2614
2814
  inputRefs.current[0]?.focus();
2615
2815
  }, []);
2616
- useEffect11(() => {
2816
+ useEffect12(() => {
2617
2817
  if (error)
2618
2818
  submittedRef.current = false;
2619
2819
  }, [error]);
2620
- useEffect11(() => {
2820
+ useEffect12(() => {
2621
2821
  if (resendCooldown <= 0)
2622
2822
  return;
2623
2823
  const timer = setTimeout(() => setResendCooldown((c) => c - 1), 1000);
@@ -2758,35 +2958,66 @@ function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
2758
2958
  }
2759
2959
 
2760
2960
  // 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 }) {
2961
+ import { jsx as jsx9, jsxs as jsxs8, Fragment as Fragment3 } from "react/jsx-runtime";
2962
+ 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";
2963
+ var MUTED_BUTTON = "skippr:bg-muted skippr:text-foreground skippr:hover:bg-muted/80";
2964
+ function MeetingControls({
2965
+ onHangUp,
2966
+ onPause,
2967
+ onResume,
2968
+ isPaused = false,
2969
+ isPausing = false,
2970
+ showScreenShareToggle = true
2971
+ }) {
2764
2972
  const { isMuted, toggleMute, isScreenSharing, toggleScreenShare } = useMediaControls();
2973
+ const showPaused = isPaused || isPausing;
2765
2974
  return /* @__PURE__ */ jsxs8("div", {
2766
2975
  className: "skippr:shrink-0 skippr:border-t skippr:border-border skippr:bg-background skippr:px-4 skippr:py-4",
2767
2976
  children: [
2768
2977
  /* @__PURE__ */ jsxs8("div", {
2769
2978
  className: "skippr:flex skippr:items-center skippr:justify-center skippr:gap-3",
2770
2979
  children: [
2771
- /* @__PURE__ */ jsx9("button", {
2980
+ !showPaused && /* @__PURE__ */ jsxs8(Fragment3, {
2981
+ children: [
2982
+ /* @__PURE__ */ jsx9("button", {
2983
+ type: "button",
2984
+ onClick: toggleMute,
2985
+ "aria-label": isMuted ? "Unmute" : "Mute",
2986
+ className: cn(CONTROL_BUTTON2, isMuted ? "skippr:bg-destructive/15 skippr:text-destructive skippr:hover:bg-destructive/25" : MUTED_BUTTON),
2987
+ children: isMuted ? /* @__PURE__ */ jsx9(MicOff, {
2988
+ className: "skippr:size-5"
2989
+ }) : /* @__PURE__ */ jsx9(Mic, {
2990
+ className: "skippr:size-5"
2991
+ })
2992
+ }),
2993
+ showScreenShareToggle && /* @__PURE__ */ jsx9("button", {
2994
+ type: "button",
2995
+ onClick: toggleScreenShare,
2996
+ "aria-label": isScreenSharing ? "Stop sharing screen" : "Share screen",
2997
+ className: cn(CONTROL_BUTTON2, isScreenSharing ? "skippr:bg-bubble skippr:text-white skippr:hover:brightness-110" : MUTED_BUTTON),
2998
+ children: isScreenSharing ? /* @__PURE__ */ jsx9(MonitorOff, {
2999
+ className: "skippr:size-5"
3000
+ }) : /* @__PURE__ */ jsx9(Monitor, {
3001
+ className: "skippr:size-5"
3002
+ })
3003
+ })
3004
+ ]
3005
+ }),
3006
+ showPaused ? onResume && /* @__PURE__ */ jsx9("button", {
2772
3007
  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, {
3008
+ onClick: onResume,
3009
+ disabled: isPausing,
3010
+ "aria-label": "Resume session",
3011
+ className: cn(CONTROL_BUTTON2, MUTED_BUTTON),
3012
+ children: /* @__PURE__ */ jsx9(Play, {
2779
3013
  className: "skippr:size-5"
2780
3014
  })
2781
- }),
2782
- showScreenShareToggle && /* @__PURE__ */ jsx9("button", {
3015
+ }) : onPause && /* @__PURE__ */ jsx9("button", {
2783
3016
  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, {
3017
+ onClick: onPause,
3018
+ "aria-label": "Pause session",
3019
+ className: cn(CONTROL_BUTTON2, MUTED_BUTTON),
3020
+ children: /* @__PURE__ */ jsx9(Pause, {
2790
3021
  className: "skippr:size-5"
2791
3022
  })
2792
3023
  }),
@@ -2810,17 +3041,17 @@ function MeetingControls({ onHangUp, showScreenShareToggle = true }) {
2810
3041
  }
2811
3042
 
2812
3043
  // src/components/MessageList.tsx
2813
- import { useEffect as useEffect13, useRef as useRef7 } from "react";
3044
+ import { useEffect as useEffect14, useRef as useRef8 } from "react";
2814
3045
 
2815
3046
  // src/components/ChatInput.tsx
2816
- import { useEffect as useEffect12, useRef as useRef6, useState as useState9 } from "react";
3047
+ import { useEffect as useEffect13, useRef as useRef7, useState as useState9 } from "react";
2817
3048
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2818
3049
  var MAX_INPUT_HEIGHT = 60;
2819
3050
  function ChatInput({ sendChatMessage, isSendingChat, autoFocus = false }) {
2820
3051
  const [inputText, setInputText] = useState9("");
2821
- const textareaRef = useRef6(null);
3052
+ const textareaRef = useRef7(null);
2822
3053
  const canSend = inputText.trim().length > 0 && !isSendingChat;
2823
- useEffect12(() => {
3054
+ useEffect13(() => {
2824
3055
  if (autoFocus)
2825
3056
  textareaRef.current?.focus();
2826
3057
  }, [autoFocus]);
@@ -2965,9 +3196,9 @@ function MessageList({
2965
3196
  isSendingChat,
2966
3197
  autoFocus = false
2967
3198
  }) {
2968
- const scrollRef = useRef7(null);
3199
+ const scrollRef = useRef8(null);
2969
3200
  const lastMessage = messages.length > 0 ? messages[messages.length - 1] : undefined;
2970
- useEffect13(() => {
3201
+ useEffect14(() => {
2971
3202
  scrollRef.current?.scrollIntoView({ behavior: "smooth" });
2972
3203
  }, [messages.length, lastMessage?.content]);
2973
3204
  const showTyping = isStreaming && lastMessage?.role === "assistant" && lastMessage.content === "";
@@ -2999,7 +3230,7 @@ function MessageList({
2999
3230
  }
3000
3231
 
3001
3232
  // src/components/ModuleSelector.tsx
3002
- import { useEffect as useEffect14, useRef as useRef8, useState as useState10 } from "react";
3233
+ import { useEffect as useEffect15, useRef as useRef9, useState as useState10 } from "react";
3003
3234
  import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
3004
3235
  var AGENT_TYPE_ICONS = {
3005
3236
  onboarding: UserPlus,
@@ -3022,10 +3253,10 @@ function ModuleSelector() {
3022
3253
  error
3023
3254
  } = useLiveAgent();
3024
3255
  const isBusy = isStarting || isDisconnecting;
3025
- const scrollRef = useRef8(null);
3256
+ const scrollRef = useRef9(null);
3026
3257
  const [showScrollHint, setShowScrollHint] = useState10(false);
3027
3258
  const [isScrolled, setIsScrolled] = useState10(false);
3028
- useEffect14(() => {
3259
+ useEffect15(() => {
3029
3260
  const el = scrollRef.current;
3030
3261
  if (!el)
3031
3262
  return;
@@ -3109,6 +3340,7 @@ function ModuleSelector() {
3109
3340
  const isFeatured = index2 === 0;
3110
3341
  const isWide = index2 === availableModules.length - 1 && availableModules.length % 2 === 1;
3111
3342
  const Icon2 = getAgentIcon(module.type);
3343
+ const canResume = canResumeSession(module.currentSession, module.mode);
3112
3344
  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
3345
  const layout = isWide ? "skippr:items-center skippr:p-3.5 skippr:pb-5" : "skippr:flex-col skippr:items-start skippr:p-3.5";
3114
3346
  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 +3360,23 @@ function ModuleSelector() {
3128
3360
  /* @__PURE__ */ jsxs13("div", {
3129
3361
  className: "skippr:min-w-0 skippr:w-full skippr:space-y-0.5",
3130
3362
  children: [
3131
- /* @__PURE__ */ jsx14("p", {
3132
- className: "skippr:line-clamp-1 skippr:text-sm skippr:font-semibold",
3133
- children: module.name
3363
+ /* @__PURE__ */ jsxs13("div", {
3364
+ className: "skippr:flex skippr:items-center skippr:gap-1.5",
3365
+ children: [
3366
+ /* @__PURE__ */ jsx14("p", {
3367
+ className: "skippr:line-clamp-1 skippr:text-sm skippr:font-semibold",
3368
+ children: module.name
3369
+ }),
3370
+ canResume && /* @__PURE__ */ jsxs13("span", {
3371
+ 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"}`,
3372
+ children: [
3373
+ /* @__PURE__ */ jsx14(Play, {
3374
+ className: "skippr:size-2.5"
3375
+ }),
3376
+ "Resume"
3377
+ ]
3378
+ })
3379
+ ]
3134
3380
  }),
3135
3381
  module.description && /* @__PURE__ */ jsx14("p", {
3136
3382
  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 +3473,27 @@ function SessionWarningBanner({ remaining }) {
3227
3473
 
3228
3474
  // src/components/StartSessionPrompt.tsx
3229
3475
  import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
3476
+ 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
3477
  function StartSessionPrompt({
3231
3478
  onStartSession,
3232
3479
  agentId,
3233
3480
  agentControls,
3234
3481
  isStarting,
3235
3482
  error,
3236
- label = "Talk to Skippr"
3483
+ label = "Talk to Skippr",
3484
+ canResume = false,
3485
+ onResume
3237
3486
  }) {
3238
3487
  return /* @__PURE__ */ jsxs15("div", {
3239
3488
  className: "skippr:flex skippr:flex-1 skippr:flex-col skippr:items-center skippr:justify-center skippr:gap-3 skippr:px-4",
3240
3489
  children: [
3241
- /* @__PURE__ */ jsx17("button", {
3490
+ canResume && onResume ? /* @__PURE__ */ jsx17("button", {
3491
+ type: "button",
3492
+ onClick: onResume,
3493
+ disabled: isStarting,
3494
+ className: PROMPT_BUTTON,
3495
+ children: isStarting ? "Resuming..." : "Resume session"
3496
+ }) : /* @__PURE__ */ jsx17("button", {
3242
3497
  type: "button",
3243
3498
  onClick: () => {
3244
3499
  if (!agentId)
@@ -3246,7 +3501,7 @@ function StartSessionPrompt({
3246
3501
  onStartSession(agentControls ? { agentId, agentControls } : { agentId });
3247
3502
  },
3248
3503
  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",
3504
+ className: PROMPT_BUTTON,
3250
3505
  children: isStarting ? "Starting..." : label
3251
3506
  }),
3252
3507
  error && /* @__PURE__ */ jsx17("p", {
@@ -3258,7 +3513,7 @@ function StartSessionPrompt({
3258
3513
  }
3259
3514
 
3260
3515
  // src/components/Sidebar.tsx
3261
- import { jsx as jsx18, jsxs as jsxs16, Fragment as Fragment3 } from "react/jsx-runtime";
3516
+ import { jsx as jsx18, jsxs as jsxs16, Fragment as Fragment4 } from "react/jsx-runtime";
3262
3517
  function Sidebar({
3263
3518
  hideControls = false,
3264
3519
  hideHeader = false,
@@ -3268,8 +3523,14 @@ function Sidebar({
3268
3523
  variant,
3269
3524
  isConnected,
3270
3525
  isStarting,
3526
+ isDisconnecting,
3527
+ isPausing,
3528
+ isPaused,
3271
3529
  error,
3272
3530
  startSession,
3531
+ pauseSession,
3532
+ resumeSession,
3533
+ resumableSession,
3273
3534
  disconnect,
3274
3535
  isPanelOpen,
3275
3536
  position,
@@ -3285,11 +3546,13 @@ function Sidebar({
3285
3546
  captureMode,
3286
3547
  hasModuleSelector,
3287
3548
  agentId,
3549
+ agentMode,
3288
3550
  agentControls
3289
3551
  } = useLiveAgent();
3552
+ const showAgenda = agentMode !== "always_on";
3290
3553
  const isFloating = variant === "floating";
3291
3554
  const isSidebar = variant === "sidebar";
3292
- useEffect15(() => {
3555
+ useEffect16(() => {
3293
3556
  if (!isSidebar)
3294
3557
  return;
3295
3558
  const prop = position === "right" ? "marginRight" : "marginLeft";
@@ -3320,7 +3583,13 @@ function Sidebar({
3320
3583
  isSubmitting: isAuthSubmitting
3321
3584
  }) : /* @__PURE__ */ jsx18(AuthenticatedContent, {
3322
3585
  isConnected,
3586
+ isPaused,
3587
+ isPausing,
3588
+ isDisconnecting,
3589
+ canResume: resumableSession !== null,
3323
3590
  onStartSession: startSession,
3591
+ onPause: pauseSession,
3592
+ onResume: resumeSession,
3324
3593
  onDisconnect: disconnect,
3325
3594
  isStarting,
3326
3595
  error,
@@ -3332,14 +3601,21 @@ function Sidebar({
3332
3601
  showScreenShareToggle: captureMode === "screenshare",
3333
3602
  hasModuleSelector,
3334
3603
  agentId,
3335
- agentControls
3604
+ agentControls,
3605
+ showAgenda
3336
3606
  })
3337
3607
  ]
3338
3608
  });
3339
3609
  }
3340
3610
  function AuthenticatedContent({
3341
3611
  isConnected,
3612
+ isPaused,
3613
+ isPausing,
3614
+ isDisconnecting,
3615
+ canResume,
3342
3616
  onStartSession,
3617
+ onPause,
3618
+ onResume,
3343
3619
  onDisconnect,
3344
3620
  isStarting,
3345
3621
  error,
@@ -3351,11 +3627,22 @@ function AuthenticatedContent({
3351
3627
  showScreenShareToggle,
3352
3628
  hasModuleSelector,
3353
3629
  agentId,
3354
- agentControls
3630
+ agentControls,
3631
+ showAgenda
3355
3632
  }) {
3356
- const showSelectorAsPrompt = hasModuleSelector && !isConnected && !isStarting;
3633
+ const inSession = isConnected || isStarting || isPaused || isPausing;
3634
+ const showSelectorAsPrompt = hasModuleSelector && !inSession && !isDisconnecting;
3357
3635
  const showTabBar = !showSelectorAsPrompt;
3358
- return /* @__PURE__ */ jsxs16(Fragment3, {
3636
+ const effectiveTab = showAgenda ? activeTab : "chat";
3637
+ let transitionLabel = null;
3638
+ if (isPausing) {
3639
+ transitionLabel = "Pausing...";
3640
+ } else if (isStarting && !isConnected) {
3641
+ transitionLabel = "Reconnecting...";
3642
+ } else if (isPaused) {
3643
+ transitionLabel = "Paused";
3644
+ }
3645
+ return /* @__PURE__ */ jsxs16(Fragment4, {
3359
3646
  children: [
3360
3647
  isConnected && /* @__PURE__ */ jsx18(ConnectedBanner, {}),
3361
3648
  showTabBar && /* @__PURE__ */ jsxs16("div", {
@@ -3375,7 +3662,7 @@ function AuthenticatedContent({
3375
3662
  })
3376
3663
  ]
3377
3664
  }),
3378
- /* @__PURE__ */ jsxs16("button", {
3665
+ showAgenda && /* @__PURE__ */ jsxs16("button", {
3379
3666
  type: "button",
3380
3667
  className: cn("skippr:relative skippr:inline-flex skippr:cursor-pointer skippr:items-center skippr:gap-1.5 skippr:rounded-lg skippr:px-3 skippr:py-2 skippr:text-sm skippr:font-medium skippr:transition-all", activeTab === "agenda" ? "skippr:text-foreground" : "skippr:text-muted-foreground skippr:hover:text-foreground"),
3381
3668
  onClick: () => onTabChange("agenda"),
@@ -3393,9 +3680,22 @@ function AuthenticatedContent({
3393
3680
  }),
3394
3681
  /* @__PURE__ */ jsx18("div", {
3395
3682
  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
3683
+ children: isDisconnecting ? /* @__PURE__ */ jsx18("div", {
3684
+ className: "skippr:flex skippr:flex-1 skippr:items-center skippr:justify-center",
3685
+ children: /* @__PURE__ */ jsx18(LoadingDots, {
3686
+ label: "Ending session..."
3687
+ })
3688
+ }) : showSelectorAsPrompt ? /* @__PURE__ */ jsx18(ModuleSelector, {}) : inSession ? /* @__PURE__ */ jsxs16(Fragment4, {
3689
+ children: [
3690
+ transitionLabel && /* @__PURE__ */ jsx18("div", {
3691
+ 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",
3692
+ children: transitionLabel
3693
+ }),
3694
+ /* @__PURE__ */ jsx18(ConnectedBody, {
3695
+ activeTab: effectiveTab,
3696
+ autoFocusChat
3697
+ })
3698
+ ]
3399
3699
  }) : /* @__PURE__ */ jsx18("div", {
3400
3700
  className: "skippr:flex skippr:min-h-0 skippr:flex-1 skippr:flex-col skippr:animate-skippr-tab-fade",
3401
3701
  children: /* @__PURE__ */ jsx18(StartSessionPrompt, {
@@ -3404,12 +3704,18 @@ function AuthenticatedContent({
3404
3704
  agentControls,
3405
3705
  isStarting,
3406
3706
  error,
3407
- label: startSessionLabel
3707
+ label: startSessionLabel,
3708
+ canResume,
3709
+ onResume
3408
3710
  })
3409
3711
  }, `${activeTab}-empty`)
3410
3712
  }),
3411
- isConnected && !hideControls && /* @__PURE__ */ jsx18(MeetingControls, {
3713
+ (isConnected || isPaused) && !isDisconnecting && !hideControls && /* @__PURE__ */ jsx18(MeetingControls, {
3412
3714
  onHangUp: onDisconnect,
3715
+ onPause,
3716
+ onResume,
3717
+ isPaused,
3718
+ isPausing,
3413
3719
  showScreenShareToggle
3414
3720
  })
3415
3721
  ]
@@ -3527,10 +3833,14 @@ function LiveAgent(props) {
3527
3833
  shouldConnect,
3528
3834
  isStarting,
3529
3835
  isDisconnecting,
3836
+ isPausing,
3530
3837
  error,
3531
3838
  errorCode,
3532
3839
  startSession,
3840
+ pauseSession,
3533
3841
  disconnect,
3842
+ isPaused,
3843
+ historyMessages,
3534
3844
  pendingScreenStream,
3535
3845
  bearerToken
3536
3846
  } = useSession({
@@ -3542,9 +3852,17 @@ function LiveAgent(props) {
3542
3852
  onStartError: expandOnSessionStartError,
3543
3853
  onDisconnect: minimizeOnSessionDisconnect
3544
3854
  });
3855
+ const teardownInFlightRef = useRef10(false);
3856
+ teardownInFlightRef.current = isPaused || isPausing || isDisconnecting;
3857
+ const handleRoomDisconnected = useCallback8(() => {
3858
+ if (teardownInFlightRef.current)
3859
+ return;
3860
+ disconnect();
3861
+ }, [disconnect]);
3545
3862
  const [isPanelOpen, setIsPanelOpen] = useState11(defaultOpen);
3546
3863
  const [isMinimized, setIsMinimized] = useState11(minimizable && !defaultOpen);
3547
3864
  const [sidebarTab, setSidebarTab] = useState11("agenda");
3865
+ const [phasesSnapshot, setPhasesSnapshot] = useState11([]);
3548
3866
  const {
3549
3867
  modules: availableModules,
3550
3868
  isLoading: isLoadingModules,
@@ -3554,12 +3872,34 @@ function LiveAgent(props) {
3554
3872
  enabled: hasModuleSelector && isPanelOpen,
3555
3873
  bearerToken
3556
3874
  });
3875
+ const activeModuleForAgent = useMemo5(() => {
3876
+ if (activeModule)
3877
+ return activeModule;
3878
+ if (!hostAgentId)
3879
+ return null;
3880
+ return availableModules.find((m) => m.id === hostAgentId) ?? null;
3881
+ }, [activeModule, hostAgentId, availableModules]);
3882
+ const agentMode = activeModuleForAgent?.mode ?? null;
3883
+ const resumableSession = useMemo5(() => getResumableSession(activeModuleForAgent), [activeModuleForAgent]);
3884
+ const resumeSession = useCallback8(async () => {
3885
+ if (!resumableSession)
3886
+ return;
3887
+ await startSession({
3888
+ agentId: resumableSession.agentId,
3889
+ agentControls,
3890
+ existingSessionId: resumableSession.id
3891
+ });
3892
+ }, [resumableSession, startSession, agentControls]);
3557
3893
  const selectModule = useCallback8((moduleId) => {
3558
3894
  const found = availableModules.find((m) => m.id === moduleId);
3559
3895
  if (!found)
3560
3896
  return;
3561
3897
  setActiveModule(found);
3562
- startSession({ agentId: found.id, agentControls: found.controls });
3898
+ startSession({
3899
+ agentId: found.id,
3900
+ agentControls: found.controls,
3901
+ existingSessionId: getResumableSessionId(found.currentSession, found.mode)
3902
+ });
3563
3903
  }, [availableModules, startSession]);
3564
3904
  const [welcomeDismissed, setWelcomeDismissed] = useState11(false);
3565
3905
  const dismissWelcome = useCallback8(() => setWelcomeDismissed(true), []);
@@ -3598,10 +3938,18 @@ function LiveAgent(props) {
3598
3938
  isConnected,
3599
3939
  isStarting,
3600
3940
  isDisconnecting,
3941
+ isPausing,
3601
3942
  error,
3602
3943
  errorCode,
3603
3944
  startSession,
3945
+ pauseSession,
3946
+ resumeSession,
3604
3947
  disconnect,
3948
+ isPaused,
3949
+ resumableSession,
3950
+ historyMessages,
3951
+ phasesSnapshot,
3952
+ setPhasesSnapshot,
3605
3953
  isPanelOpen,
3606
3954
  openPanel,
3607
3955
  closePanel,
@@ -3625,6 +3973,7 @@ function LiveAgent(props) {
3625
3973
  autoFocusChat,
3626
3974
  captureMode,
3627
3975
  agentId,
3976
+ agentMode,
3628
3977
  agentControls,
3629
3978
  hasModuleSelector,
3630
3979
  availableModules,
@@ -3639,10 +3988,17 @@ function LiveAgent(props) {
3639
3988
  isConnected,
3640
3989
  isStarting,
3641
3990
  isDisconnecting,
3991
+ isPausing,
3642
3992
  error,
3643
3993
  errorCode,
3644
3994
  startSession,
3995
+ pauseSession,
3996
+ resumeSession,
3645
3997
  disconnect,
3998
+ isPaused,
3999
+ resumableSession,
4000
+ historyMessages,
4001
+ phasesSnapshot,
3646
4002
  isPanelOpen,
3647
4003
  openPanel,
3648
4004
  closePanel,
@@ -3665,6 +4021,7 @@ function LiveAgent(props) {
3665
4021
  autoFocusChat,
3666
4022
  captureMode,
3667
4023
  agentId,
4024
+ agentMode,
3668
4025
  agentControls,
3669
4026
  hasModuleSelector,
3670
4027
  availableModules,
@@ -3681,7 +4038,7 @@ function LiveAgent(props) {
3681
4038
  token: connection?.token,
3682
4039
  connect: shouldConnect,
3683
4040
  audio: true,
3684
- onDisconnected: disconnect,
4041
+ onDisconnected: handleRoomDisconnected,
3685
4042
  children: [
3686
4043
  connection && /* @__PURE__ */ jsx20(RoomAudioRenderer, {}),
3687
4044
  connection && captureMode === "screenshare" && /* @__PURE__ */ jsx20(AutoStartMedia, {